Go 并行性和并發(fā)性:有什么不同?
大家好,我是程序員幽鬼。
并發(fā)和并行,Go 剛發(fā)布時(shí),官方就不斷強(qiáng)調(diào)這兩點(diǎn)的不同??赡苄率忠廊幻院?。這次給大家弄一個(gè)系列,詳細(xì)講解并發(fā)和并行。
軟件中的并行性是同時(shí)執(zhí)行指令。每種編程語(yǔ)言要么實(shí)現(xiàn)自己的庫(kù),要么提供語(yǔ)言級(jí)支持,如 Go。并行性允許軟件工程師通過(guò)在多個(gè)處理器上并行執(zhí)行任務(wù)來(lái)回避硬件的物理限制。
由于正確利用并行構(gòu)建的復(fù)雜性,應(yīng)用程序的并行性取決于構(gòu)建軟件的工程師的技能 。
并行任務(wù)示例:
- 多人在餐廳點(diǎn)單
- 雜貨店的多個(gè)收銀員
- 多核 CPU
實(shí)際上,任何應(yīng)用程序都存在多層并行性。有應(yīng)用程序本身的并行度,由應(yīng)用程序開發(fā)者定義,還有 CPU 在操作系統(tǒng)編排的物理硬件上執(zhí)行的指令的并行度(或多路復(fù)用)。
1、并行性的構(gòu)建
應(yīng)用程序開發(fā)人員利用抽象來(lái)描述應(yīng)用程序的并行性。這些抽象在實(shí)現(xiàn)并行性但概念相同的每種語(yǔ)言中通常是不同的。例如,在 C 中,并行性是通過(guò)使用 pthreads 來(lái)定義的 ,而在 Go 中,并行性是通過(guò)使用 goroutines 來(lái)定義的 。
進(jìn)程
進(jìn)程是一個(gè)執(zhí)行單元,包括它自己的“程序計(jì)數(shù)器、寄存器和變量。從概念上講,每個(gè)進(jìn)程都有自己的虛擬 CPU”。理解這一點(diǎn)很重要,因?yàn)閯?chuàng)建和管理進(jìn)程會(huì)產(chǎn)生開銷。除了創(chuàng)建進(jìn)程的開銷外,每個(gè)進(jìn)程只能訪問(wèn)自己的內(nèi)存。這意味著該進(jìn)程無(wú)法訪問(wèn)其他進(jìn)程的內(nèi)存。
如果有多個(gè)執(zhí)行線程(并行任務(wù))需要訪問(wèn)某些共享資源,這將是一個(gè)問(wèn)題。
線程
引入線程是為了在同一進(jìn)程內(nèi)但在不同的并行執(zhí)行單元上授予對(duì)共享內(nèi)存的訪問(wèn)權(quán)限。線程幾乎是它們自己的進(jìn)程,但可以訪問(wèn)父進(jìn)程的共享地址空間。
線程的開銷遠(yuǎn)低于進(jìn)程,因?yàn)樗鼈儾槐貫槊總€(gè)線程創(chuàng)建一個(gè)新進(jìn)程,并且資源可以共享或重用。
以下是 Ubuntu 18.04 的示例,比較了 fork 進(jìn)程和創(chuàng)建線程的開銷:
- # 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ū)是進(jìn)程中各種并行任務(wù)所需的共享內(nèi)存區(qū)。這些部分可能是共享數(shù)據(jù)、類型或其他資源。
并行的復(fù)雜性
由于進(jìn)程的線程在相同的內(nèi)存空間中執(zhí)行,因此存在多個(gè)線程同時(shí)訪問(wèn)臨界區(qū)的風(fēng)險(xiǎn)。這可能會(huì)導(dǎo)致應(yīng)用程序中的數(shù)據(jù)損壞或其他意外行為。
當(dāng)多個(gè)線程同時(shí)訪問(wèn)共享內(nèi)存時(shí),會(huì)出現(xiàn)兩個(gè)主要問(wèn)題。
競(jìng)態(tài)條件
競(jìng)態(tài)條件是多個(gè)并行執(zhí)行線程在沒(méi)有任何保護(hù)的情況下直接讀取或?qū)懭牍蚕碣Y源。這可能導(dǎo)致存儲(chǔ)在資源中的數(shù)據(jù)可能被損壞或?qū)е缕渌馔庑袨榈那闆r。
例如,想象一個(gè)進(jìn)程,其中單個(gè)線程正在從共享內(nèi)存位置讀取值,而另一個(gè)線程正在將新值寫入同一位置。如果第一個(gè)線程在第二個(gè)線程寫入值之前讀取該值,則第一個(gè)線程將讀取舊值。
這會(huì)導(dǎo)致應(yīng)用程序未按預(yù)期運(yùn)行的情況。
死鎖
當(dāng)兩個(gè)或多個(gè)線程互相等待做某事時(shí),就會(huì)發(fā)生死鎖。這可能導(dǎo)致應(yīng)用程序掛起或崩潰。
例如,一個(gè)線程針對(duì)一個(gè)臨界區(qū)執(zhí)行等待滿足條件,而另一個(gè)線程針對(duì)同一臨界區(qū)執(zhí)行并等待來(lái)自另一個(gè)線程的條件滿足。如果第一個(gè)線程正在等待滿足條件,而第二個(gè)線程正在等待第一個(gè)線程,則兩個(gè)線程將永遠(yuǎn)等待。
當(dāng)試圖通過(guò)使用互斥鎖來(lái)防止競(jìng)爭(zhēng)條件時(shí),可能會(huì)發(fā)生第二種形式的死鎖。
屏障(Barriers)
屏障是同步點(diǎn),用于管理進(jìn)程內(nèi)多個(gè)線程對(duì)共享資源或臨界區(qū)的訪問(wèn)。
這些屏障允許應(yīng)用程序開發(fā)人員控制并行訪問(wèn),以確保不會(huì)以不安全的方式訪問(wèn)資源。
互斥鎖
互斥鎖是一種屏障,它一次只允許一個(gè)線程訪問(wèn)共享資源。這對(duì)于在讀取或?qū)懭牍蚕碣Y源時(shí)通過(guò)鎖定解鎖來(lái)防止競(jìng)爭(zhēng)條件很有用。
- // 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 變量受到互斥鎖的保護(hù)。這意味著一次只有一個(gè)線程可以訪問(wèn)該 shared 變量。這確保了shared 變量不會(huì)被破壞并且行為可預(yù)測(cè)。
注意:使用互斥鎖時(shí),確保在函數(shù)返回時(shí)釋放互斥鎖至關(guān)重要。例如,在 Go 中,可以通過(guò)使用defer關(guān)鍵字來(lái)完成。這確保了其他線程(goroutine)可以訪問(wèn)共享資源。
信號(hào)量
信號(hào)量是一種屏障,它一次只允許一定數(shù)量的線程訪問(wèn)共享資源。這與互斥鎖的不同之處在于,可以訪問(wèn)資源的線程數(shù)不限于一個(gè)。
Go 標(biāo)準(zhǔn)庫(kù)中沒(méi)有信號(hào)量實(shí)現(xiàn)。但是可以使用通道來(lái)實(shí)現(xiàn)。
忙等待(busy waiting)
忙等待是一種線程等待滿足條件的技術(shù)。通常用于等待計(jì)數(shù)器達(dá)到某個(gè)值。
- // 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)
- }
- }
所以,忙等待需要一個(gè)循環(huán),該循環(huán)等待滿足讀取或?qū)懭牍蚕碣Y源的條件,并且必須由互斥鎖保護(hù)以確保正確的行為。
上述示例的問(wèn)題是循環(huán)訪問(wèn)不受互斥鎖保護(hù)的臨界區(qū)。這可能導(dǎo)致循環(huán)訪問(wèn)該值但它可能已被進(jìn)程的另一個(gè)線程更改的競(jìng)態(tài)條件。事實(shí)上,上面的例子也是競(jìng)態(tài)條件的一個(gè)很好的例子。這個(gè)應(yīng)用程序可能永遠(yuǎn)不會(huì)退出,因?yàn)椴荒鼙WC循環(huán)足夠快以讀取 x=1 時(shí)的值,這意味著循環(huán)永遠(yuǎn)不會(huì)退出。
如果我們用互斥鎖保護(hù)變量x,循環(huán)將被保護(hù),應(yīng)用程序?qū)⑼顺觯@仍然不完美,循環(huán)設(shè)置x仍然足夠快,可以在讀取值的循環(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()
- }
- }
一般來(lái)說(shuō),忙等待不是一個(gè)好辦法。最好使用信號(hào)量或互斥鎖來(lái)確保臨界區(qū)受到保護(hù)。我們將介紹在 Go 中處理此問(wèn)題的更好方法,但它說(shuō)明了編寫“正確”可并行代碼的復(fù)雜性。
WaitGroup
WaitGroup 是確保所有并行代碼路徑在繼續(xù)之前已完成處理的方法。在 Go 中,這是通過(guò)使用標(biāo)準(zhǔn)庫(kù)中 sync.WaitGroup 來(lái)完成的。
- // 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 < N; i++ {
- go func() {
- defer wg.Done()
- // do some work
- }()
- }
- // wait for all of the goroutines to finish
- wg.Wait()
- }
在上面的示例中,wg.Wait() 是一個(gè)阻塞調(diào)用。這意味著主線程將不會(huì)繼續(xù),直到所有 goroutine 中的 defer wg.Done() 都調(diào)用。在內(nèi)部,WaitGroup 是一個(gè)計(jì)數(shù)器,對(duì)于添加到wg.Add(N)調(diào)用的 WaitGroup 中的每個(gè) goroutine,它都會(huì)加一。當(dāng)計(jì)數(shù)器為零時(shí),主線程將繼續(xù)處理,或者在這種情況下應(yīng)用程序?qū)⑼顺觥?/p>
2、什么是并發(fā)?
并發(fā)性和并行性經(jīng)?;鞛橐徽?。為了更好地理解并發(fā)和并行之間的區(qū)別,讓我們看一個(gè)現(xiàn)實(shí)世界中的并發(fā)示例。
如果我們以一家餐館為例,那么就有幾組不同的工作類型(或可復(fù)制的程序)發(fā)生在一家餐館中。
- 主管(負(fù)責(zé)安排客人入座)
- 服務(wù)員(負(fù)責(zé)接單和提供食物)
- 廚房(負(fù)責(zé)烹飪食物)
- Bussers(負(fù)責(zé)清理桌子)
- 洗碗機(jī)(負(fù)責(zé)清理餐具)
這些小組中的每一個(gè)都負(fù)責(zé)不同的任務(wù),所有這些最終都會(huì)導(dǎo)致顧客吃到一頓飯,這稱為并發(fā)。 專門的工作中心可以專注于單個(gè)任務(wù),這些任務(wù)結(jié)合起來(lái)會(huì)產(chǎn)生結(jié)果。
如果餐廳每項(xiàng)任務(wù)只雇用一個(gè)人,餐廳的效率就會(huì)受到限制。這稱為序列化。如果餐廳只有一個(gè)服務(wù)員,那么一次只能接受一個(gè)訂單。
并行性是處理并發(fā)任務(wù)并將它們分布在多個(gè)資源中的能力。在餐廳,這將包括服務(wù)員、食物準(zhǔn)備和清潔。如果有多個(gè)服務(wù)器,則可以一次接受多個(gè)訂單。
每個(gè)小組都能夠?qū)W⒂谒麄兊奶囟üぷ髦行?,而不必?fù)?dān)心上下文切換、最大化吞吐量或最小化延遲。
具有并行工作中心的行業(yè)的其他示例包括工廠工人和裝配線工人。本質(zhì)上,任何可以分解為更小的可重復(fù)任務(wù)的過(guò)程都可以被認(rèn)為是并發(fā)的,因此在使用適當(dāng)?shù)牟l(fā)設(shè)計(jì)時(shí)可以并行化。
TL;DR: 并發(fā)可以實(shí)現(xiàn)正確的并行性,但并行代碼不需要并行性。
原文鏈接:https://benjiv.com/parallelism-vs-concurrency/
本文轉(zhuǎn)載自微信公眾號(hào)「幽鬼」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系幽鬼公眾號(hào)。