新聞中心
并發(fā) VS 并行
在講解并發(fā)概念時(shí),總會(huì)涉及另外一個(gè)概念并行。下面讓我們來(lái)聊聊并發(fā)和并行之間的區(qū)別。

創(chuàng)新互聯(lián)是一家專注于成都網(wǎng)站建設(shè)、成都網(wǎng)站制作與策劃設(shè)計(jì),大理州網(wǎng)站建設(shè)哪家好?創(chuàng)新互聯(lián)做網(wǎng)站,專注于網(wǎng)站建設(shè)十多年,網(wǎng)設(shè)計(jì)領(lǐng)域的專業(yè)建站公司;建站業(yè)務(wù)涵蓋:大理州等地區(qū)。大理州做網(wǎng)站價(jià)格咨詢:13518219792
- 并發(fā)(concurrency):把任務(wù)在不同的時(shí)間點(diǎn)交給處理器進(jìn)行處理。在同一時(shí)間點(diǎn),任務(wù)并不會(huì)同時(shí)運(yùn)行。
- 并行(parallelism):把每一個(gè)任務(wù)分配給每一個(gè)處理器獨(dú)立完成。在同一時(shí)間點(diǎn),任務(wù)一定是同時(shí)運(yùn)行。
并發(fā)不是并行。并行是讓不同的代碼片段同時(shí)在不同的物理處理器上執(zhí)行。并行的關(guān)鍵是同時(shí)做很多事情,而并發(fā)是指同時(shí)管理很多事情,這些事情可能只做了一半就被暫停去做別的事情了。
在很多情況下,并發(fā)的效果比并行好,因?yàn)椴僮飨到y(tǒng)和硬件的總資源一般很少,但能支持系統(tǒng)同時(shí)做很多事情。這種“使用較少的資源做更多的事情”的哲學(xué),也是指導(dǎo) Go語(yǔ)言設(shè)計(jì)的哲學(xué)。
如果希望讓 goroutine 并行,必須使用多于一個(gè)邏輯處理器。當(dāng)有多個(gè)邏輯處理器(CPU)時(shí),調(diào)度器會(huì)將 goroutine 平等分配到每個(gè)邏輯處理器上。這會(huì)讓 goroutine 在不同的線程上運(yùn)行。不過(guò)要想真的實(shí)現(xiàn)并行的效果,用戶需要讓自己的程序運(yùn)行在有多個(gè)物理處理器的機(jī)器上。否則,哪怕 Go語(yǔ)言運(yùn)行時(shí)使用多個(gè)線程,goroutine 依然會(huì)在同一個(gè)物理處理器上并發(fā)運(yùn)行,達(dá)不到并行的效果。
下圖展示了在一個(gè)邏輯處理器上并發(fā)運(yùn)行 goroutine 和在兩個(gè)邏輯處理器上并行運(yùn)行兩個(gè)并發(fā)的 goroutine 之間的區(qū)別。 調(diào)度器包含一些聰明的算法,這些算法會(huì)隨著Go語(yǔ)言的發(fā)布被更新和改進(jìn),所以不推薦盲目修改語(yǔ)言運(yùn)行時(shí)對(duì)邏輯處理器的默認(rèn)設(shè)置。如果真的認(rèn)為修改邏輯處理器的數(shù)量可以改進(jìn)性能,也可以對(duì)語(yǔ)言運(yùn)行時(shí)的參數(shù)進(jìn)行細(xì)微調(diào)整。
并發(fā)與并行的區(qū)別
Go 可以充分發(fā)揮多核優(yōu)勢(shì),高效運(yùn)行。 Go語(yǔ)言在 GOMAXPROCS 數(shù)量與任務(wù)數(shù)量相等時(shí),可以做到并行執(zhí)行,但一般情況下都是并發(fā)執(zhí)行。
目錄
- 1.1 Goroutine
- 1.2 CSP
- 1.3 Channel
- 1.4 Lock
- 1.5 WaitGroup
1.1 Goroutine
由誰(shuí)創(chuàng)建?
- 線程是操作系統(tǒng)分配給應(yīng)用程序的獨(dú)立執(zhí)行單元,它們可以在多核處理器中并行執(zhí)行。線程的調(diào)度是由操作系統(tǒng)內(nèi)核負(fù)責(zé)的,并且線程之間有獨(dú)立的地址空間。
- 協(xié)程是由程序員編寫(xiě)的,它是一種輕量級(jí)的線程,并由Go語(yǔ)言運(yùn)行時(shí)管理。協(xié)程之間沒(méi)有獨(dú)立的地址空間,而是共享一個(gè)地址空間。協(xié)程的調(diào)度是由Go語(yǔ)言運(yùn)行時(shí)負(fù)責(zé)的,并且可以在單個(gè)線程中并行執(zhí)行。
線程的創(chuàng)建和銷毀的開(kāi)銷比較大,而協(xié)程的創(chuàng)建和銷毀開(kāi)銷很小,因此在需要高并發(fā)的場(chǎng)景中,使用協(xié)程更加高效。
大小比較?
線程棧是由操作系統(tǒng)分配的,它通常有一個(gè)固定的大小,并且在線程創(chuàng)建時(shí)分配。它存儲(chǔ)著線程的狀態(tài)信息和調(diào)用棧。線程棧的大小取決于操作系統(tǒng)的限制,一般在幾百KB到幾MB之間。
而協(xié)程的棧是由Go語(yǔ)言運(yùn)行時(shí)管理的,它通常有一個(gè)較小的默認(rèn)大小,并在協(xié)程創(chuàng)建時(shí)分配。它也存儲(chǔ)著協(xié)程的狀態(tài)信息和調(diào)用棧。協(xié)程棧的大小可以通過(guò)Golang的runtime包中的函數(shù)來(lái)調(diào)整,一般在幾KB到幾MB之間。
由于協(xié)程的棧比線程棧小,所以協(xié)程能夠創(chuàng)建的數(shù)量比線程多得多。但是由于協(xié)程棧比線程棧小,所以在調(diào)用深度較深的程序中,協(xié)程可能會(huì)爆棧。
1.2 CSP
CSP:Communicating Sequential Processes
Go語(yǔ)言提倡:通過(guò)通信共享內(nèi)存,而不是通過(guò)共享內(nèi)存而實(shí)現(xiàn)通信。
有緩沖通道
緩沖通道中的數(shù)字表示該通道可以在沒(méi)有接收者阻塞的情況下緩存多少個(gè)元素。
加入容量為1,所以只能緩存一個(gè)元素。如果一個(gè)新的元素試圖被發(fā)送到已經(jīng)滿了的通道中,發(fā)送者將會(huì)阻塞直到接收者從通道中讀取一個(gè)元素。
阻塞并不一定意味著數(shù)據(jù)丟失,這取決于阻塞的原因和應(yīng)用程序的設(shè)計(jì):
在 Go 語(yǔ)言中,通道是一種同步機(jī)制,發(fā)送者和接收者之間可以通過(guò)通道來(lái)進(jìn)行通信。 如果發(fā)送者試圖向一個(gè)滿的緩沖通道發(fā)送數(shù)據(jù),那么發(fā)送者將會(huì)阻塞直到緩沖區(qū)有空間可用。同樣,如果接收者試圖從一個(gè)空的通道接收數(shù)據(jù),那么接收者將會(huì)阻塞直到通道中有數(shù)據(jù)可用。這種情況下,數(shù)據(jù)不會(huì)丟失,而是在緩沖區(qū)中等待被取出。
無(wú)緩沖通道
但是,如果通道是無(wú)緩沖的,那么發(fā)送者和接收者之間將是同步的。如果發(fā)送者在接收者準(zhǔn)備好之前發(fā)送了數(shù)據(jù),那么發(fā)送者將會(huì)阻塞直到接收者準(zhǔn)備好。
如果接收者在數(shù)據(jù)可用之前就開(kāi)始接收,那么接收者將會(huì)阻塞直到數(shù)據(jù)可用。在這種情況下,如果發(fā)送者和接收者之間的時(shí)間差較大,那么可能會(huì)導(dǎo)致數(shù)據(jù)丟失。
所以阻塞并不一定意味著數(shù)據(jù)丟失,而是取決于程序是否設(shè)計(jì)了阻塞的處理方式,以及阻塞的類型。
下面是一個(gè)示例代碼,其中兩個(gè) goroutine 通過(guò)緩沖通道共享內(nèi)存:
package main
import (
"fmt"
)
func main() {
// 創(chuàng)建緩沖通道
ch := make(chan int, 1)
// 啟動(dòng)第一個(gè)goroutine
go func() {
for i := 0; i < 10; i++ {
ch <- i // 發(fā)送數(shù)據(jù)
}
close(ch) // 關(guān)閉通道
}()
// 啟動(dòng)第二個(gè)goroutine
go func() {
for i := range ch {
fmt.Println(i) // 接收數(shù)據(jù)并打印
}
}()
// 等待所有g(shù)oroutine結(jié)束
fmt.Scanln()
}
執(zhí)行效果:
在這個(gè)示例中,第一個(gè) goroutine 會(huì)循環(huán)發(fā)送 0 到 9 的整數(shù),而第二個(gè) goroutine 會(huì)接收這些整數(shù)并打印。這兩個(gè) goroutine 都會(huì)共享同一個(gè)通道來(lái)傳遞數(shù)據(jù)。
注意,在生產(chǎn)環(huán)境中,通常需要使用同步機(jī)制來(lái)等待 goroutine 結(jié)束,而不是使用 fmt.Scanln()。
1.3 Channel
make(chan 元素類型,[緩沖大小])
- 無(wú)緩沖通道 make(chan int) 同步
- 有緩沖通道 make(chan int,2) 不同步
無(wú)緩沖通道是在發(fā)送者和接收者之間同步地傳遞消息。 發(fā)送者會(huì)在接收者準(zhǔn)備好接收消息之前阻塞,接收者會(huì)在接收到消息之前阻塞。這種方式可以保證消息的順序和每個(gè)消息只被接收一次。
緩沖通道具有一個(gè)固定大小的緩沖區(qū),發(fā)送者和接收者之間不再是同步的。 如果緩沖區(qū)已滿,發(fā)送者會(huì)繼續(xù)執(zhí)行而不會(huì)阻塞;如果緩沖區(qū)為空,接收者會(huì)繼續(xù)執(zhí)行而不會(huì)阻塞。這種方式可以提高程序的性能,但是可能會(huì)導(dǎo)致消息的丟失或重復(fù)。
package main
import (
"fmt"
)
func main() {
// 創(chuàng)建通道
ch := make(chan int)
ch_squared := make(chan int)
// 啟動(dòng)A子協(xié)程
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}()
// 啟動(dòng)B子協(xié)程
go func() {
for i := range ch {
ch_squared <- i * i
}
close(ch_squared)
}()
//主協(xié)程輸出結(jié)果
for i := range ch_squared {
fmt.Println(i)
}
}
執(zhí)行效果:
在這個(gè)程序中,A子協(xié)程循環(huán)發(fā)送0~9的數(shù)字,B子協(xié)程接收并計(jì)算數(shù)字的平方,最后主協(xié)程等待所有子協(xié)程完成后輸出所有數(shù)字的平方。
注意:
- 在這個(gè)程序中我們使用了兩個(gè)通道ch, ch_squared來(lái)傳遞數(shù)據(jù),以避免數(shù)據(jù)丟失。
- 在最后輸出結(jié)果時(shí),主協(xié)程要等待所有子協(xié)程完成,因此我們使用了 for i := range ch_squared來(lái)等待子協(xié)程的完成
- 在生產(chǎn)環(huán)境中,通常需要使用同步機(jī)制來(lái)等待子協(xié)程結(jié)束,而不是使用 for i := range ch_squared。
- 可以把ch_squared改為帶緩沖的channe,以解決生產(chǎn)比消費(fèi)快的執(zhí)行效率問(wèn)題。
1.4 并發(fā)安全 Lock
在并發(fā)編程中,當(dāng)多個(gè) goroutine 同時(shí)訪問(wèn)共享資源時(shí),可能會(huì)出現(xiàn)競(jìng)爭(zhēng)條件,導(dǎo)致數(shù)據(jù)不一致或錯(cuò)誤。為了避免這種情況,我們可以使用 Lock(鎖)來(lái)保證并發(fā)安全。
Lock 是一種同步機(jī)制,可以防止多個(gè) goroutine 同時(shí)訪問(wèn)共享資源。當(dāng)一個(gè) goroutine 獲取鎖時(shí),其他 goroutine 將被阻塞,直到鎖被釋放。
Go語(yǔ)言標(biāo)準(zhǔn)庫(kù)中提供了 sync.Mutex 來(lái)實(shí)現(xiàn)鎖。
一個(gè)簡(jiǎn)單的例子:
package main
import (
"fmt"
"sync"
)
var (
count int
lock sync.Mutex
)
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
lock.Lock()
defer lock.Unlock()
count++
fmt.Println(count)
}()
}
wg.Wait()
}
執(zhí)行效果:
在上面的示例中,main函數(shù)中啟動(dòng)了10個(gè)goroutine,每個(gè)goroutine都會(huì)嘗試去獲取鎖,并對(duì)共享變量count進(jìn)行修改。在獲取鎖后才能進(jìn)行修改,其他goroutine在等待鎖時(shí)將被阻塞。
這樣就能保證并發(fā)安全了,使得共享變量count在多個(gè)goroutine之間可以安全地訪問(wèn)。但是,使用鎖也需要注意避免死鎖的情況,需要在適當(dāng)?shù)臅r(shí)候釋放鎖。并發(fā)安全問(wèn)題難以定位。
1.5 WaitGroup
Go語(yǔ)言標(biāo)準(zhǔn)庫(kù)中提供了 sync.WaitGroup 來(lái)管理多個(gè) goroutine 的執(zhí)行。
- Add(delta int): 使用該方法來(lái)增加等待組中 goroutine 的數(shù)量。當(dāng)我們需要等待一些 goroutine 執(zhí)行完畢時(shí),就可以使用該方法來(lái)增加等待組中 goroutine 的數(shù)量。
- Done(): 使用該方法來(lái)通知等待組,一個(gè) goroutine 執(zhí)行完畢。當(dāng)一個(gè) goroutine 執(zhí)行完畢后,我們需要調(diào)用該方法來(lái)通知等待組。
- Wait(): 使用該方法來(lái)等待等待組中的所有 goroutine 執(zhí)行完畢。當(dāng)我們需要等待所有 goroutine 執(zhí)行完畢時(shí),就可以使用該方法。
下面是一個(gè)例子,演示了如何使用 sync.WaitGroup 來(lái)管理多個(gè) goroutine 的執(zhí)行:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(3) //增加3個(gè)goroutine
go func() {
defer wg.Done()
fmt.Println("Goroutine 1")
}()
go func() {
defer wg.Done()
fmt.Println("Goroutine 2")
}()
go func() {
defer wg.Done()
fmt.Println("Goroutine 3")
}()
wg.Wait()
fmt.Println("all goroutines have been finished")
}
執(zhí)行效果:
在上面的代碼中,我們使用了 sync.WaitGroup 來(lái)管理三個(gè) goroutine 的執(zhí)行。我們先使用 wg.Add(3) 來(lái)增加等待組中 goroutine 的數(shù)量。然后在每個(gè) goroutine 中調(diào)用 wg.Done() 來(lái)通知等待組,該 goroutine 執(zhí)行完畢。最后使用 wg.Wait() 來(lái)等待所有 goroutine 執(zhí)行完畢。
注意:
- 如果沒(méi)有 wg.Wait(),主協(xié)程可能會(huì)在其他協(xié)程還沒(méi)有執(zhí)行完成的情況下結(jié)束,這樣的話其他協(xié)程的執(zhí)行結(jié)果就沒(méi)有機(jī)會(huì)被獲取。
- 如果Add的數(shù)量和done的數(shù)量不對(duì)應(yīng),wait永遠(yuǎn)不會(huì)返回,這也叫死鎖。
在線運(yùn)行
上面分享的代碼都支持,訪問(wèn)下方鏈接運(yùn)行測(cè)試:https://1024code.com/codecubes/GB47x7u
本文轉(zhuǎn)載自微信公眾號(hào)「 程序員升級(jí)打怪之旅」,作者「王中陽(yáng)Go」,可以通過(guò)以下二維碼關(guān)注。
轉(zhuǎn)載本文請(qǐng)聯(lián)系「 程序員升級(jí)打怪之旅」公眾號(hào)。
分享文章:「Go語(yǔ)言進(jìn)階」并發(fā)編程詳解
標(biāo)題來(lái)源:http://www.dlmjj.cn/article/dppcioi.html


咨詢
建站咨詢
