Go中并發(fā)控制的實現(xiàn)方式總結(jié)
Go的并發(fā)控制
在Go實際開發(fā)中,并發(fā)安全是老生常談的事情,在并發(fā)下,goroutine之間的存在數(shù)據(jù)資源等方面的競爭。
為了保證數(shù)據(jù)一致性、防止死鎖等問題的出現(xiàn),在并發(fā)中需要使用一些方式來實現(xiàn)并發(fā)控制。
并發(fā)控制的目的是確保在多個并發(fā)執(zhí)行的線程或進程中,對共享資源的訪問和操作能夠正確、有效地進行,并且避免出現(xiàn)競態(tài)條件和數(shù)據(jù)不一致的問題。
在Go中,可以通過以下幾種方式來實現(xiàn)并發(fā)控制:
1、channel
channel通道主要用于于goroutine之間通信和同步的機制。通過使用channel,可以在不同的goroutine之間進行數(shù)據(jù)的發(fā)送與接收,從而實現(xiàn)協(xié)調(diào)和控制并發(fā),以達到并發(fā)控制。
根據(jù)channel的類型,可以實現(xiàn)不同的并發(fā)控制效果:
無緩沖channel
當使用make初始化時,不指定channel的容量大小,即初始化無緩沖channel;
當發(fā)送方向無緩沖channel發(fā)送消息數(shù)據(jù)時,如果發(fā)送后channel的數(shù)據(jù)未被接收方獲取,則當前goroutine會阻塞在發(fā)送語句中,直到有接收者準備好接收數(shù)據(jù)為止,即無緩沖通道要求發(fā)送操作和接收操作同時準備好才能完成通信。這樣做是確保了發(fā)送和接收的同步,避免了數(shù)據(jù)競爭和不確定性。
package main
import (
"fmt"
"time"
)
func main() {
// 創(chuàng)建一個無緩沖通道
ch := make(chan int)
// 啟動一個 goroutine 接收數(shù)據(jù)
go func() {
time.Sleep(time.Second * 5)
fmt.Println("等待接收數(shù)據(jù)")
data := <-ch // 接收數(shù)據(jù)
fmt.Println("接收到數(shù)據(jù):", data)
}()
fmt.Println("發(fā)送數(shù)據(jù)")
// 發(fā)送數(shù)據(jù),由于匿名函數(shù)goroutine睡眠,無緩沖通道內(nèi)數(shù)據(jù)沒有g(shù)oroutine接收,因此會阻塞。5s后被接收則繼續(xù)執(zhí)行
ch <- 100
time.Sleep(time.Second)
fmt.Println("程序結(jié)束")
}
在上述代碼中,創(chuàng)建了一個無緩沖通道
ch。然后在一個單獨的goroutine中啟動了一個接收操作,等待從通道ch中接收數(shù)據(jù)。接下來,在
main goroutine中執(zhí)行發(fā)送操作,向通道ch發(fā)送數(shù)據(jù)100。由于無緩沖通道的特性,當發(fā)送語句
ch <- 100執(zhí)行時,由于沒有接收者準備好接收數(shù)據(jù)(單獨的goroutine處于5s睡眠),發(fā)送操作會被阻塞。接收方的
goroutine在接收數(shù)據(jù)之前會一直等待。當接收方的
goroutine準備好之后,發(fā)送操作完成,數(shù)據(jù)被成功發(fā)送并被接收方接收,然后程序繼續(xù)執(zhí)行后續(xù)語句,打印出相應(yīng)的輸出。
需要注意的是,在使用無緩沖channel時,如果沒有接收者,發(fā)送操作將會永久阻塞,可能會導(dǎo)致死鎖,因此在使用無緩沖通道時,需要確保發(fā)送和接收操作能夠匹配。
有緩沖channel
當使用make初始化時,可以指定channel的容量大小,即初始化有緩沖channel,通道的容量表示通道中最大能存放的元素數(shù)量。
當發(fā)送方發(fā)送數(shù)據(jù)到有緩存
channel時,如果緩沖區(qū)滿了,則發(fā)送方會被阻塞直到有緩沖空間可以接收這個消息數(shù)據(jù);當接收方在有緩沖
channel接收數(shù)據(jù)時,如果緩沖區(qū)為空,則接收方會被阻塞直到channel有數(shù)據(jù)可讀;
無論是緩存 channel 還是無緩沖 channel,都是并發(fā)安全的,即多個 goroutine 可以同時發(fā)送和接收數(shù)據(jù),而不需要額外的同步機制。
但是,由于緩存 channel 具有緩存空間,因此在使用時需要特別注意緩存空間的大小,避免過度消耗內(nèi)存或者發(fā)生死鎖等問題。
2、sync.WaitGroup
在sync包中,sync.WaitGroup可以在并發(fā)goroutine之間起到執(zhí)行屏障的效果。WaitGroup提供了用于創(chuàng)建多個goroutine時,能夠等待多個并發(fā)執(zhí)行的代碼塊在達到WaitGroup顯示指定的同步條件后,才可以繼續(xù)執(zhí)行Wait的后續(xù)代碼。在使用sync.WaitGroup實現(xiàn)同步模式下,從而起到并發(fā)控制的效果。
在Go中,sync.WaitGroup類型提供了如下幾個方法:
| 方法名 | 功能說明 |
|---|---|
func (wg * WaitGroup) Add(delta int) | 等待組計數(shù)器 + delta |
(wg *WaitGroup) Done() | 等待組計數(shù)器-1 |
(wg *WaitGroup) Wait() | 阻塞直到等待組計數(shù)器變?yōu)? |
示例:
package main
import (
"fmt"
"sync"
)
// 聲明全局等待組變量
var wg sync.WaitGroup
func printHello() {
fmt.Println("Hello World")
wg.Done() // 完成一個任務(wù)后,調(diào)用Done()方法,等待組減1,告知當前goroutine已經(jīng)完成任務(wù)
}
func main() {
wg.Add(1) // 等待組加1,表示登記一個goroutine
go printHello()
fmt.Println("main")
wg.Wait() // 阻塞當前goroutine,直到等待組中的所有g(shù)oroutine都完成任務(wù)
}
// 執(zhí)行結(jié)果
main
Hello World
3、sync.Mutex
sync.Mutex 是 Go 語言中的一個互斥鎖(Mutex)類型,用于實現(xiàn)對共享資源的互斥訪問。
互斥鎖是一種常見的并發(fā)控制機制,它能夠確保在同一時刻只有一個 goroutine 可以訪問被保護的資源,從而避免數(shù)據(jù)競爭和不確定的結(jié)果。
互斥鎖的作用可以有以下幾個方面:
- 保護共享資源:當多個
goroutine并發(fā)訪問共享資源時,通過使用互斥鎖可以限制只有一個goroutine可以訪問共享資源,從而避免競態(tài)條件和數(shù)據(jù)不一致的問題。 - 實現(xiàn)臨界區(qū):互斥鎖可以將一段代碼標記為臨界區(qū),只有獲取了鎖的
goroutine才能執(zhí)行該臨界區(qū)的代碼,其他goroutine則需要等待解鎖,才能夠訪問臨界區(qū)內(nèi)的代碼塊。
互斥鎖的基本使用方式是,通過調(diào)用 Lock() 方法獲取鎖,執(zhí)行臨界區(qū)代碼,然后調(diào)用 Unlock() 方法釋放鎖。在獲取鎖之后,其他 goroutine 將會被阻塞,直到當前 goroutine 釋放鎖為止。Lock() 方法與Unlock() 底層的實現(xiàn)原理是使用原子操作來維護Mutex的state狀態(tài)。
sync.Mutex中,除了最基本的互斥鎖外,還提供讀寫鎖,在讀多寫少的場景下,相比互斥鎖性能上能夠有所提升。
channel 與 Mutex 對比例子
在自增操作x++中,該操作并非原子操作,因此在多個goroutine對全局變量x進行自增時,會出現(xiàn)數(shù)據(jù)覆蓋的情況,因此可以通過一些方法來實現(xiàn)并發(fā)控制,例如channel、互斥鎖、原子操作。
可以對比一下channel與互斥鎖在實現(xiàn)并發(fā)控制時的執(zhí)行時間:
- 使用
channel
package main
import (
"fmt"
"sync"
"time"
)
var x int64
var wg sync.WaitGroup
func main() {
startTime := time.Now()
ch := make(chan struct{}, 1)
for i := 0; i < 10000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
ch <- struct{}{}
x++
<-ch
}()
}
wg.Wait()
endTime := time.Now()
fmt.Println(x) // 10000
fmt.Println(endTime.Sub(startTime)) // 6.2933ms
}
- 使用
Mutex
package main
import (
"fmt"
"sync"
"time"
)
var x int64
var wg sync.WaitGroup
var lock sync.Mutex
func main() {
startTime := time.Now()
for i := 0; i < 10000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
lock.Lock()
x++
lock.Unlock()
}()
}
wg.Wait()
endTime := time.Now()
fmt.Println(x) // 10000
fmt.Println(endTime.Sub(startTime)) // 3.0835ms
}
可以對比兩種方法的執(zhí)行時間,在啟動10000個goroutine執(zhí)行10000次全局變量x++時,channel實現(xiàn)并發(fā)控制全局變量x++的執(zhí)行時間為6.2933ms(存在波動),而使用Mutex提供的互斥鎖實現(xiàn)并發(fā)控制全局變量x++的執(zhí)行時間為3.0835ms(存在波動),大約在兩倍左右,這是為什么呢?
原因在于channel的操作涉及到**goroutine之間的調(diào)度和上下文的切換**,而互斥鎖底層使用了Go的原子操作,執(zhí)行時間較短,因為互斥鎖的操作相對輕量,不涉及goroutine的調(diào)度以及上下文的切換。
在開發(fā)過程中,選擇使用通道還是互斥鎖取決于具體的場景與需求,并不是一定說使用鎖就好,需要根據(jù)實際的業(yè)務(wù)場景來進行選擇。如果需要更細粒度的控制和更高的并發(fā)性能,可以優(yōu)先考慮使用互斥鎖。
4、atomic原子操作
Go語言提供了原子操作用于對內(nèi)存中的變量進行同步訪問,避免了多個goroutine同時訪問同一個變量時可能產(chǎn)生的競態(tài)條件。
sync/atomic包提供了原子加操作、比較并交換等方法提供一系列原子操作,這些方法利用底層的原子指令,確保對內(nèi)存中的變量進行原子級別的訪問和修改,從而實現(xiàn)并發(fā)控制。
package main
import (
"fmt"
"sync"
"sync/atomic"
)
var x int64
var wg sync.WaitGroup
// 使用原子操作
func atomicAdd() {
atomic.AddInt64(&x, 1)
wg.Done()
}
func main() {
for i := 0; i < 10000; i++ {
wg.Add(1)
go atomicAdd() // 原子操作add函數(shù)
}
wg.Wait()
fmt.Println(x) // 10000
}
一些常用的原子操作函數(shù):
Add函數(shù):AddInt32、AddInt64、AddUint32、AddUint64等方法,用于對變量進行原子加操作。CompareAndSwap函數(shù):CompareAndSwapInt32、CompareAndSwapInt64、CompareAndSwapUint32、CompareAndSwapUint64等,用于比較并交換操作,當舊值等于給定值時,將新值賦值到指定地址中。Load函數(shù):LoadInt32、LoadInt64、LoadUint32、LoadUint64等,用于加載操作,返回指定地址中存儲的值。Store函數(shù):StoreInt32、StoreInt64、StoreUint32、StoreUint64等,用于存儲操作,將給定的值存儲到指定地址中。Swap函數(shù):SwapInt32、SwapInt64、SwapUint32、SwapUint64等,用于交換操作,將指定地址中存儲的值和給定的值進行交換,并返回原值。
以上就是Go中并發(fā)控制的實現(xiàn)方式總結(jié)的詳細內(nèi)容,更多關(guān)于Go并發(fā)控制實現(xiàn)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go 并發(fā)實現(xiàn)協(xié)程同步的多種解決方法
這篇文章主要介紹了Go 并發(fā)——實現(xiàn)協(xié)程同步的多種解決方法,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下2018-08-08

