<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          『每周譯Go』以Go為例-探究并行與并發(fā)的區(qū)別

          共 2385字,需瀏覽 5分鐘

           ·

          2022-02-12 10:03

          在軟件內(nèi)并行是指多條指令同時執(zhí)行。每個編程語言都有各自實現(xiàn)并行,或者像Go,將并行作為語言的一部分,提供原生支持。并行讓軟件工程師能夠同時在多核處理器上并行執(zhí)行任務(wù),從而拋開硬件的物理限制。1

          通常情況下,由于構(gòu)建并行模塊的復(fù)雜性,一個應(yīng)用程序的并行程度取決于工程師編寫軟件的能力。

          并行任務(wù)的例子:

          • 多人同時在餐廳點單
          • 多個收銀員在雜貨鋪
          • 多核CPU

          事實上,在任何一個應(yīng)用程序中都有多層含義的并行。有應(yīng)用程序本身的并行,這是由應(yīng)用程序開發(fā)人員定義的,還有由操作系統(tǒng)協(xié)調(diào)的物理硬件上的CPU執(zhí)行的指令的并行(或復(fù)用)。

          注意:一般情況下,應(yīng)用程序必須明確寫出他們使用并行。這個需要工程師需要有技能寫出”正確”的可并行的代碼。

          構(gòu)建并行

          應(yīng)用程序開發(fā)人員利用抽象概念來描述一個應(yīng)用程序的并行。這些抽象概念通常在每個實現(xiàn)并行的編程語言上會有所不同,但是概念是一樣的。舉個例子,在C語言,并行是通過pthread來定義的。在Go,并行是通過goroutines來定義的。

          進程

          一個進程是一個單一的執(zhí)行單元,包含它自己的”程序計數(shù)器,寄存器和變量”。從概念上來講,每個進程有它自己的虛擬CPU”2。這一點很重要,因為涉及到進程在創(chuàng)建和管理過程中的開銷。除了創(chuàng)建進程時的開銷,每個進程只允許訪問自己的內(nèi)存。這表示進程不能訪問其他進程的內(nèi)存。

          如果多個執(zhí)行線程(并行任務(wù))需要訪問一些共享資源時,這會是一個問題。

          線程

          線程是作為一種方法被引入的,它允許在同一進程中訪問共享內(nèi)存,但在不同的并行執(zhí)行單元上。線程基本上是自己的進程,但是可以訪問父進程的共享地址空間。

          線程相較于進程只需要更少的開銷,因為它們不需要為了每個線程創(chuàng)建新進程,并且資源可以被共享或者復(fù)用。

          這里有一個在Ubuntu 18.04下,克隆進程和創(chuàng)建線程的開銷比較:3

          #?Borrowed?from?https://stackoverflow.com/a/52231151/834319
          #?Ubuntu?18.04?start_method:?fork
          #?================================
          results?for?Process:

          count????1000.000000
          mean????????0.002081
          std?????????0.000288
          min?????????0.001466
          25%?????????0.001866
          50%?????????0.001973
          75%?????????0.002268
          max?????????0.003365?

          Minimum?with?1.47?ms
          ------------------------------------------------------------

          results?for?Thread:

          count????1000.000000
          mean????????0.000054
          std?????????0.000013
          min?????????0.000044
          25%?????????0.000047
          50%?????????0.000051
          75%?????????0.000058
          max?????????0.000319?

          Minimum?with?43.89?μs
          ------------------------------------------------------------
          Minimum?start-up?time?for?processes?takes?33.41x?longer?than?for?threads.

          臨界區(qū)

          臨界區(qū)是共享的內(nèi)存部分,它被進程中的各種并行任務(wù)所需要。這個部分可能是共享數(shù)據(jù),類型或者資源。(見下方的范例4)

          并行的復(fù)雜性

          由于一個進程的線程在同一內(nèi)存空間中執(zhí)行,因此存在著臨界區(qū)被多個線程同時訪問的風(fēng)險。在應(yīng)用程序中這個可能導(dǎo)致數(shù)據(jù)損壞或其他無法預(yù)料的行為。

          這里有2個主要問題當(dāng)多個線程同一時間訪問共享內(nèi)存的時候。

          競態(tài)條件

          舉個例子,想象一個進程的線程正在從一個共享內(nèi)存地址讀取一個數(shù)值,同時其他線程正在往同一個地址寫一個新的數(shù)值。如果第一個線程在第二個線程寫數(shù)值之前讀取了數(shù)值,第一個線程就會讀取到舊的數(shù)值。

          這會導(dǎo)致應(yīng)用程序出現(xiàn)不符合預(yù)期的情況。

          死鎖

          當(dāng)兩個或多個線程在互相等待對方做某事時,就會出現(xiàn)死鎖。這會導(dǎo)致應(yīng)用程序掛起或者崩潰。

          有一個例子是這樣的,當(dāng)一個線程等待一個時機去執(zhí)行臨界區(qū)的同時,另一個線程也正在等待其他線程滿足條件后去執(zhí)行相同的臨界區(qū)。如果第一個線程正在等待滿足時機,然后第二個線程也正在等待第一個線程,那這兩個線程將一直等待下去。

          第二種形式的死鎖會發(fā)生在嘗試使用互斥鎖保護競態(tài)。

          屏障

          屏障可以稱為一個同步點,它管理一個進程中多個線程對共享資源或臨界區(qū)的訪問。

          這些屏障允許應(yīng)用程序開發(fā)者去控制并行訪問,從而保證資源不會在不安全的情況下被訪問。

          互斥鎖(Mutexes)

          互斥鎖是屏障的一個類型,它只允許一個線程在同一時間訪問共享資源。這對于防止在讀取或?qū)懭牍蚕碣Y源時通過鎖定和解鎖出現(xiàn)競態(tài)的情況非常有用。

          //?Example?of?a?mutex?barrier?in?Go
          import?(
          ??"sync"
          ??"fmt"
          )

          var?shared?string
          var?sharedMu?sync.Mutex

          func?main()?{

          ??//?Start?a?goroutine?to?write?to?the?shared?variable
          ??go?func()?{
          ????for?i?:=?0;?i???????write(fmt.Sprintf("%d",?i))
          ????}
          ??}()

          ??//?read?from?the?shared?variable
          ??for?i?:=?0;?i?????read(fmt.Sprintf("%d",?i))
          ??}
          }

          func?write(value?string)?{
          ??sharedMu.Lock()
          ??defer?sharedMu.Unlock()

          ??//?set?a?new?value?for?the?`shared`?variable
          ??shared?=?value
          }

          func?read()?{
          ??sharedMu.Lock()
          ??defer?sharedMu.Unlock()

          ??//?print?the?critical?section?`shared`?to?stdout
          ??fmt.Println(shared)
          }

          如果我們看上面的例子,我們可以看到shared變量被互斥鎖保護著。這意味著只有一個線程在一個時間點可以訪問shared變量。這個保證了shared變量不被損壞,并且是一個可預(yù)計的行為。

          注意: 在使用互斥鎖時,需要注意的一個點是,要在函數(shù)返回的時候釋放互斥鎖。在Go,舉個例子,這個操作可以通過關(guān)鍵字defer實現(xiàn)。這個保證了其他線程可以訪問到共享資源。

          信號量

          信號量是一種類型的屏障,允許一個時間點一定數(shù)量的線程訪問共享資源。這個和互斥鎖的區(qū)別在于,訪問資源的線程數(shù)量不會被限制為1個。

          在Go標(biāo)準(zhǔn)庫沒有信號的實現(xiàn),但是可以通過channels5來實現(xiàn)。

          忙等待

          忙等待是一個技術(shù)用于線程等待一個滿足的條件。通常用于等待一個計數(shù)器達到某個數(shù)值。

          //?Example?of?Busy?Waiting?in?Go
          var?x?int

          func?main()?{
          ??go?func()?{
          ????for?i?:=?0;?i???????x?=?i
          ????}
          ??}()

          ??for?x?!=?1?{?//?Loop?until?x?is?set?to?1
          ????fmt.Println("Waiting...")
          ????time.Sleep(time.Millisecond?*?100)
          ??}??
          }

          因此,忙等待需要一個等待條件滿足的循環(huán),該循環(huán)對共享資源進行讀取或?qū)懭耄仨氂梢粋€互斥鎖來保護以確保正確的行為。

          上面例子的問題是那個循環(huán)在訪問一個沒有被互斥鎖保護的臨界區(qū)。這可能導(dǎo)致競態(tài),這個循環(huán)讀取的數(shù)值可能已經(jīng)被另一個進程里的線程修改了。事實上,上面的例子是一個很好的競態(tài)例子。很有可能這個應(yīng)用程序永遠(yuǎn)都不會退出,因為無法保證這個循環(huán)是否會足夠快地讀取到x的數(shù)值,同時讀取出來的數(shù)值都是1,這就意味著循環(huán)永遠(yuǎn)不會退出。

          如果我們要用互斥鎖保護變量x,那么循環(huán)就會被保護并且應(yīng)用程序會退出,但這仍然不完美,設(shè)置x的循環(huán)仍然可以快到在讀取值的循環(huán)執(zhí)行之前擊中互斥鎖兩次(盡管不太可能)。

          import?"sync"

          var?x?int
          var?xMu?sync.Mutex

          func?main()?{
          ??go?func()?{
          ????for?i?:=?0;?i???????xMu.Lock()
          ??????x?=?i
          ??????xMu.Unlock()
          ????}
          ??}()

          ??var?value?int
          ??for?value?!=?1?{?//?Loop?until?x?is?set?to?1
          ????xMu.Lock()
          ????value?=?x?//?Set?value?==?x
          ????xMu.Unlock()
          ??}??
          }

          通常情況下忙等待不是一個好的想法。最好的辦法是使用信號或者一個互斥鎖去確保臨界區(qū)是受保護的。我們將介紹在Go中處理這個問題的更好方法,但它說明了編寫 “正確的”可并行代碼的復(fù)雜性。

          等待組(Wait Groups)

          等待組是一個用來保證所有并行代碼路徑在繼續(xù)之前完成處理的方法。在Go里,這個用標(biāo)準(zhǔn)庫中的sync包中提供的sync.WaitGroup來實現(xiàn)。

          //?Example?of?a?`sync.WaitGroup`?in?Go
          import?(
          ??"sync"
          )

          func?main()?{
          ??var?wg?sync.WaitGroup
          ??var?N?int?=?10

          ??wg.Add(N)
          ??for?i?:=?0;?i?????go?func()?{
          ??????defer?wg.Done()
          ??????
          ??????//?do?some?work??????
          ????}()
          ??}

          ??//?wait?for?all?of?the?goroutines?to?finish
          ??wg.Wait()
          }

          在上面這個例子的wg.Wait()是一個阻塞調(diào)用。這個表示主線程會等到所有協(xié)程完成后再繼續(xù)執(zhí)行,并且對應(yīng)的defer wg.Done()已經(jīng)被調(diào)用。WaitGroup的內(nèi)部實現(xiàn)是一個計數(shù)器,當(dāng)每個協(xié)程在調(diào)用wg.Add(N)后會加1,同時協(xié)程被加到WaitGroup內(nèi)。當(dāng)計數(shù)器計到0,主線程會繼續(xù)執(zhí)行或者在這個例子中會退出。

          什么是并發(fā)?

          并發(fā)和并行經(jīng)?;鞛橐徽劇榱烁玫乩斫獠l(fā)和并行的區(qū)別,讓我們看一個現(xiàn)實生活中的并發(fā)例子。

          如果我們用餐廳來當(dāng)做例子,餐廳里面會有幾種不同工作類型(或可復(fù)制的程序)的組別。

          1. 接待(負(fù)責(zé)為客人安排座位)
          2. 服務(wù)員(負(fù)責(zé)接單,并提供食物)
          3. 廚房(負(fù)責(zé)烹飪食物)
          4. 售貨員(負(fù)責(zé)清理桌子
          5. 洗碗工(負(fù)責(zé)清理餐具) 每個組別負(fù)責(zé)不同的任務(wù),所有這些任務(wù)的最終結(jié)果都是讓顧客吃到一頓飯。這稱之為并發(fā),專門的工作中心,可以專注于單獨的任務(wù),這些任務(wù)結(jié)合起來就會產(chǎn)生一個結(jié)果。

          如果餐廳只雇傭一個員工來做所有的任務(wù),這對于一個高效率的餐廳是一個限制。這稱之為序列化。如果在餐廳里只有一個服務(wù)員,那么在一個時間只能夠處理一個訂單。

          并行性是指將并發(fā)的任務(wù)分配到多個資源上的能力。在餐廳中,這可能會包含服務(wù),食物準(zhǔn)備和清理。如果有多個服務(wù)員,那么同一時間就可以處理多個訂單。

          每個組可以專注在他們自己的工作中心,不需要擔(dān)心上下文切換,最大吞吐量,或最小延遲。

          其他有同時進行的工作中心的行業(yè)例子包括工廠工人和裝配線工人。從本質(zhì)上講,任何可以被分解成較小的可重復(fù)任務(wù)的過程都可以被認(rèn)為是并發(fā)的,因此當(dāng)使用合適的并發(fā)設(shè)計的時候可以被并行處理。

          TL:DR:并發(fā)實現(xiàn)正確的并行,但是并行對并發(fā)代碼不是必要的。6


          1. Andrew S. Tanenbaum and Herbert Bos, Modern Operating Systems (Boston, MA: Prentice Hall, 2015), 517. ??
          2. Andrew S. Tanenbaum and Herbert Bos, Modern Operating Systems (Boston, MA: Prentice Hall, 2015), 86. ??
          3. Benchmarking Process Fork vs Thread Creation on Ubuntu 18.04
            (https://stackoverflow.com/a/52231151/834319)??
          4. Flowgraph description of critical section - Kartikharia
            (https://commons.wikimedia.org/wiki/File:Critical_section_fg.jpg)??
          5. Example semaphore implementation in Go
            (http://www.golangpatterns.info/concurrency/semaphores) ??
          6. https://youtu.be/oV9rvDllKEg??



          原文信息

          原文地址:https://benjiv.com/parallelism-vs-concurrency/

          原文作者:Benjamin Vesterby

          本文永久鏈接:https://github.com/gocn/translator/blob/master/2022/w05_Parallelism_and_Concurrency_What’s_the_Difference.md

          譯者:zxmfke

          校對:Cluas




          想要了解關(guān)于 Go 的更多資訊,還可以通過掃描的方式,進群一起探討哦~



          瀏覽 55
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  中日韩成人片 | 做爱网站免费猛烈 | 国产综合色图 | 成人毛片网 | 国产内射视频在线观看 |