詳解Go并發(fā)編程時如何避免發(fā)生競態(tài)條件和數(shù)據(jù)競爭
會發(fā)生競態(tài)條件和數(shù)據(jù)競爭的場景有哪些
- 多個 goroutine 對同一變量進行讀寫操作。例如,多個 goroutine 同時對一個計數(shù)器變量進行增加操作。
- 多個 goroutine 同時對同一數(shù)組、切片或映射進行讀寫操作。例如,多個 goroutine 同時對一個切片進行添加或刪除元素的操作。
- 多個 goroutine 同時對同一文件進行讀寫操作。例如,多個 goroutine 同時向同一個文件中寫入數(shù)據(jù)。
- 多個 goroutine 同時對同一網(wǎng)絡連接進行讀寫操作。例如,多個 goroutine 同時向同一個 TCP 連接中寫入數(shù)據(jù)。
- 多個 goroutine 同時對同一通道進行讀寫操作。例如,多個 goroutine 同時向同一個無緩沖通道中發(fā)送數(shù)據(jù)或接收數(shù)據(jù)。
所以,我們要明白的一點是:只要多個 goroutine 并發(fā)訪問了共享資源,就有可能出現(xiàn)競態(tài)條件和數(shù)據(jù)競爭。
避坑辦法
現(xiàn)在,我們已經(jīng)知道了。在編寫并發(fā)程序時,如果不謹慎,沒有考慮清楚共享資源的訪問方式和同步機制,那么就會發(fā)生競態(tài)條件和數(shù)據(jù)競爭這些問題,那么如何避免踩坑?避免發(fā)生競態(tài)條件和數(shù)據(jù)競爭的辦法有哪些?請看下面:
- 互斥鎖:使用 sync 包中的 Mutex 或者 RWMutex,通過對共享資源加鎖來保證同一時間只有一個 goroutine 訪問。
- 讀寫鎖:使用 sync 包中的 RWMutex,通過讀寫鎖的機制來允許多個 goroutine 同時讀取共享資源,但是只允許一個 goroutine 寫入共享資源。
- 原子操作:使用 sync/atomic 包中提供的原子操作,可以對共享變量進行原子操作,從而保證不會出現(xiàn)競態(tài)條件和數(shù)據(jù)競爭。
- 通道:使用 Go 語言中的通道機制,可以將數(shù)據(jù)通過通道傳遞,從而避免直接對共享資源的訪問。
- WaitGroup:使用 sync 包中的 WaitGroup,可以等待多個 goroutine 完成后再繼續(xù)執(zhí)行,從而保證多個 goroutine 之間的順序性。
- Context:使用 context 包中的 Context,可以傳遞上下文信息并控制多個 goroutine 的生命周期,從而避免出現(xiàn)因為某個 goroutine 阻塞導致整個程序阻塞的情況。
實戰(zhàn)場景
1.互斥鎖
比如在一個Web服務器中,多個goroutine需要同時訪問同一個全局計數(shù)器的變量,達到記錄網(wǎng)站訪問量的目的。
在這種情況下,如果沒有對訪問計數(shù)器的訪問進行同步和保護,就會出現(xiàn)競態(tài)條件和數(shù)據(jù)競爭的問題。假設有兩個goroutine A和B,它們同時讀取計數(shù)器變量的值為N,然后都增加了1并把結(jié)果寫回計數(shù)器,那么最終的計數(shù)器值只會增加1而不是2,這就是一個競態(tài)條件。
為了解決這個問題,可以使用鎖等機制來保證訪問計數(shù)器的同步和互斥。在Go中,可以使用互斥鎖(sync.Mutex)來保護共享資源。當一個goroutine需要訪問共享資源時,它需要先獲取鎖,然后訪問資源并完成操作,最后釋放鎖。這樣就可以保證每次只有一個goroutine能夠訪問共享資源,從而避免競態(tài)條件和數(shù)據(jù)競爭問題。
看下面的代碼:
package main
import (
"fmt"
"sync"
)
var count int
var mutex sync.Mutex
func main() {
var wg sync.WaitGroup
// 啟動10個goroutine并發(fā)增加計數(shù)器的值
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
// 獲取鎖
mutex.Lock()
// 訪問計數(shù)器并增加值
count++
// 釋放鎖
mutex.Unlock()
wg.Done()
}()
}
// 等待所有goroutine執(zhí)行完畢
wg.Wait()
// 輸出計數(shù)器的最終值
fmt.Println(count)
}在上面的代碼中,使用了互斥鎖來保護計數(shù)器變量的訪問。每個goroutine在訪問計數(shù)器變量之前先獲取鎖,然后進行計數(shù)器的增加操作,最后釋放鎖。這樣就可以保證計數(shù)器變量的一致性和正確性,避免競態(tài)條件和數(shù)據(jù)競爭問題。
具體的思路是,啟動每個 goroutine 時調(diào)用 wg.Add(1) 來增加等待組的計數(shù)器。然后,在所有 goroutine 執(zhí)行完畢后,調(diào)用 wg.Wait() 來等待它們完成。最后,輸出計數(shù)器的最終值。
請注意,這個假設的場景和這個代碼示例,僅僅只是是為了演示如何使用互斥鎖來保護共享資源,實際情況可能更加復雜。例如,在實際的運維開發(fā)中,如果使用鎖的次數(shù)過多,可能會影響程序的性能。因此,在實際開發(fā)中,還需要根據(jù)具體情況選擇合適的同步機制來保證并發(fā)程序的正確性和性能。
2.讀寫鎖
下面是一個使用 sync 包中的 RWMutex 實現(xiàn)讀寫鎖的代碼案例:
package main
import (
"fmt"
"sync"
"time"
)
var (
count int
rwLock sync.RWMutex
)
func readData() {
// 讀取共享數(shù)據(jù),獲取讀鎖
rwLock.RLock()
defer rwLock.RUnlock()
fmt.Println("reading data...")
time.Sleep(1 * time.Second)
fmt.Printf("data is %d\n", count)
}
func writeData(n int) {
// 寫入共享數(shù)據(jù),獲取寫鎖
rwLock.Lock()
defer rwLock.Unlock()
fmt.Println("writing data...")
time.Sleep(1 * time.Second)
count = n
fmt.Printf("data is %d\n", count)
}
func main() {
// 啟動 5 個讀取協(xié)程
for i := 0; i < 5; i++ {
go readData()
}
// 啟動 2 個寫入?yún)f(xié)程
for i := 0; i < 2; i++ {
go writeData(i + 1)
}
// 等待所有協(xié)程結(jié)束
time.Sleep(5 * time.Second)
}在這個示例中,有 5 個讀取協(xié)程和 2 個寫入?yún)f(xié)程,它們都會訪問一個共享的變量 count。讀取協(xié)程使用 RLock() 方法獲取讀鎖,寫入?yún)f(xié)程使用 Lock() 方法獲取寫鎖。通過讀寫鎖的機制,多個讀取協(xié)程可以同時讀取共享數(shù)據(jù),而寫入?yún)f(xié)程則會等待讀取協(xié)程全部結(jié)束后才能執(zhí)行,從而避免了讀取協(xié)程在寫入?yún)f(xié)程執(zhí)行過程中讀取到臟數(shù)據(jù)的問題。
3.原子操作
下面是一個使用 sync/atomic 包中提供的原子操作實現(xiàn)并發(fā)安全的計數(shù)器的代碼案例:
package main
import (
"fmt"
"sync/atomic"
"time"
)
func main() {
var counter int64
// 啟動 10 個協(xié)程對計數(shù)器進行增量操作
for i := 0; i < 10; i++ {
go func() {
for j := 0; j < 100; j++ {
atomic.AddInt64(&counter, 1)
}
}()
}
// 等待所有協(xié)程結(jié)束
time.Sleep(time.Second)
// 輸出計數(shù)器的值
fmt.Printf("counter: %d\n", atomic.LoadInt64(&counter))
}在這個示例中,有 10 個協(xié)程并發(fā)地對計數(shù)器進行增量操作。由于多個協(xié)程同時對計數(shù)器進行操作,如果不使用同步機制,就會出現(xiàn)競態(tài)條件和數(shù)據(jù)競爭。為了保證程序的正確性和健壯性,使用了 sync/atomic 包中提供的原子操作,通過 AddInt64() 方法對計數(shù)器進行原子加操作,保證了計數(shù)器的并發(fā)安全。最后使用 LoadInt64() 方法獲取計數(shù)器的值并輸出。
4.通道
下面是一個使用通道機制實現(xiàn)并發(fā)安全的計數(shù)器的代碼案例:
package main
import (
"fmt"
"sync"
)
func main() {
var counter int
// 創(chuàng)建一個有緩沖的通道,容量為 10
ch := make(chan int, 10)
// 創(chuàng)建一個等待組,用于等待所有協(xié)程完成
var wg sync.WaitGroup
wg.Add(10)
// 啟動 10 個協(xié)程對計數(shù)器進行增量操作
for i := 0; i < 10; i++ {
go func() {
for j := 0; j < 10; j++ {
// 將增量操作發(fā)送到通道中
ch <- 1
}
// 任務完成,向等待組發(fā)送信號
wg.Done()
}()
}
// 等待所有協(xié)程完成
wg.Wait()
// 從通道中接收增量操作并累加到計數(shù)器中
for i := 0; i < 100; i++ {
counter += <-ch
}
// 輸出計數(shù)器的值
fmt.Printf("counter: %d\n", counter)
}在這個示例中,有 10 個協(xié)程并發(fā)地對計數(shù)器進行增量操作。為了避免直接對共享資源的訪問,使用了一個容量為 10 的有緩沖通道,將增量操作通過通道傳遞,然后在主協(xié)程中從通道中接收增量操作并累加到計數(shù)器中。在協(xié)程中使用了等待組等待所有協(xié)程完成任務,保證了程序的正確性和健壯性。最后輸出計數(shù)器的值。
5.WaitGroup
下面是一個使用 sync.WaitGroup 等待多個 Goroutine 完成后再繼續(xù)執(zhí)行的代碼案例:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // 計數(shù)器加1
go func(i int) {
defer wg.Done() // 完成時計數(shù)器減1
fmt.Printf("goroutine %d is running\n", i)
}(i)
}
wg.Wait() // 等待所有 Goroutine 完成
fmt.Println("all goroutines have completed")
}在這個示例中,有 3 個 Goroutine 并發(fā)執(zhí)行,使用 wg.Add(1) 將計數(shù)器加1,表示有一個 Goroutine 需要等待。在每個 Goroutine 中使用 defer wg.Done() 表示任務完成,計數(shù)器減1。最后使用 wg.Wait() 等待所有 Goroutine 完成任務,然后輸出 "all goroutines have completed"。
6.Context
下面是一個使用 context.Context 控制多個 Goroutine 的生命周期的代碼案例:
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d started\n", id)
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d stopped\n", id)
return
default:
fmt.Printf("Worker %d is running\n", id)
time.Sleep(time.Second)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(ctx, i, &wg)
}
time.Sleep(3 * time.Second)
cancel()
wg.Wait()
fmt.Println("All workers have stopped")
}在這個示例中,使用 context.WithCancel 創(chuàng)建了一個上下文,并在 main 函數(shù)中傳遞給多個 Goroutine。每個 Goroutine 在一個 for 循環(huán)中執(zhí)行任務,如果收到了 ctx.Done() 信號就結(jié)束任務并退出循環(huán),否則就打印出正在運行的信息并等待一段時間。在 main 函數(shù)中,通過調(diào)用 cancel() 來發(fā)送一個信號,通知所有 Goroutine 結(jié)束任務。使用 sync.WaitGroup 等待所有 Goroutine 結(jié)束任務,然后輸出 "All workers have stopped"。
到此這篇關于詳解Go并發(fā)編程時如何避免發(fā)生競態(tài)條件和數(shù)據(jù)競爭的文章就介紹到這了,更多相關Go并發(fā)編程內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
使用Golang實現(xiàn)加權負載均衡算法的實現(xiàn)代碼
這篇文章主要介紹了使用Golang實現(xiàn)加權負載均衡算法的實現(xiàn)代碼,詳細說明權重轉(zhuǎn)發(fā)算法的實現(xiàn),通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-09-09
在Golang中正確的修改HTTPRequest的Host的操作方法
我們工作中經(jīng)常需要通過HTTP請求Server的服務,比如腳本批量請求接口跑數(shù)據(jù),由于一些網(wǎng)關策略,部分Server會要求請求中Header里面附帶Host參數(shù),所以本文給大家介紹了如何在Golang中正確的修改HTTPRequest的Host,需要的朋友可以參考下2023-12-12

