掃二維碼與項目經(jīng)理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
在講解并發(fā)概念時,總會涉及另外一個概念并行。下面讓我們來聊聊并發(fā)和并行之間的區(qū)別。

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

我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流