『每周譯Go』以Go為例-探究并行與并發(fā)的區(qū)別
在軟件內(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?10;?i++?{
??????write(fmt.Sprintf("%d",?i))
????}
??}()
??//?read?from?the?shared?variable
??for?i?:=?0;?i?10;?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?10;?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?10;?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ù)制的程序)的組別。
接待(負(fù)責(zé)為客人安排座位) 服務(wù)員(負(fù)責(zé)接單,并提供食物) 廚房(負(fù)責(zé)烹飪食物) 售貨員(負(fù)責(zé)清理桌子 洗碗工(負(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
Andrew S. Tanenbaum and Herbert Bos, Modern Operating Systems (Boston, MA: Prentice Hall, 2015), 517. ?? Andrew S. Tanenbaum and Herbert Bos, Modern Operating Systems (Boston, MA: Prentice Hall, 2015), 86. ?? Benchmarking Process Fork vs Thread Creation on Ubuntu 18.04 (https://stackoverflow.com/a/52231151/834319)?? Flowgraph description of critical section - Kartikharia (https://commons.wikimedia.org/wiki/File:Critical_section_fg.jpg)?? Example semaphore implementation in Go (http://www.golangpatterns.info/concurrency/semaphores) ?? https://youtu.be/oV9rvDllKEg??

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