Go多線程中數(shù)據(jù)不一致問(wèn)題的解決方案(sync鎖機(jī)制)
Go語(yǔ)言中的Sync鎖
在Go語(yǔ)言的并發(fā)編程中,如何確保多個(gè)goroutine安全地訪問(wèn)共享資源是一個(gè)關(guān)鍵問(wèn)題。Go語(yǔ)言提供了sync包,其中包含了多種同步原語(yǔ),用于解決并發(fā)編程中的同步問(wèn)題。本文將詳細(xì)介紹sync包中的鎖機(jī)制,并結(jié)合實(shí)際案例,幫助讀者理解和使用這些鎖。
要想解決臨界資源安全的問(wèn)題,很多編程語(yǔ)言的解決方案都是同步。
通過(guò)上鎖的方式,某一時(shí)間段,只能允許一個(gè)goroutine來(lái)訪問(wèn)這個(gè)共享數(shù)據(jù),當(dāng)前goroutine訪問(wèn)完畢, 解鎖后,其他的goroutine才 能來(lái)訪問(wèn)。
我們可以借助于sync包下的鎖操作。 synchronization
但是實(shí)際上,在Go的并發(fā)編程中有一句很經(jīng)典的話:不要以共享內(nèi)存的方式去通信:鎖,而要以通信的方式去共享內(nèi)存。
共享內(nèi)存的方式
鎖:多個(gè)線程拿的是同一個(gè)鑰匙,go語(yǔ)言不建議使用鎖機(jī)制來(lái)解決。不要以共享內(nèi)存的方式去通信
而要以通信的方式去共享內(nèi)存 go語(yǔ)言更建議我們使用 chan(通道) 來(lái)解決安全問(wèn)題。(后面會(huì)學(xué))
在Go語(yǔ)言中并不鼓勵(lì)用鎖保護(hù)共享狀態(tài)的方式,在不同的Goroutine中分享信息(以共享內(nèi)存的方式去通信)。
而是鼓勵(lì)通過(guò)channeI將共享狀態(tài)或共享狀態(tài)的變化在各個(gè)Goroutine之間傳遞(以通信的方式去共享內(nèi)存),這樣同樣能像用鎖一樣保證在同一的時(shí)間只有一個(gè)Goroutine訪問(wèn)共享狀態(tài)。
當(dāng)然,在主流的編程語(yǔ)言中為了保證多線程之間共享數(shù)據(jù)安全性和一致性,都會(huì)提供一套基本的同步工具集,如鎖,條件變量,原子操作等等。
Go語(yǔ)言標(biāo)準(zhǔn)庫(kù)也毫不意外的提供了這些同步機(jī)制,使用方式也和其他語(yǔ)言也差不多。
一、互斥鎖(Mutex)
互斥鎖(sync.Mutex)是最基本的同步機(jī)制之一,用于確保同一時(shí)間只有一個(gè)goroutine能夠訪問(wèn)特定的資源。
當(dāng)一個(gè)goroutine持有互斥鎖時(shí),其他試圖獲取該鎖的goroutine將會(huì)被阻塞,直到鎖被釋放。
1.1 基本用法
package main import ( "fmt" "sync" "time" ) // 定義全局變量 票庫(kù)存為10張 var tickets int = 10 // 定義一個(gè)鎖 Mutex 鎖頭 var mutex sync.Mutex func main() { go saleTicket("張三") go saleTicket("李四") go saleTicket("王五") go saleTicket("趙六") time.Sleep(time.Second * 5) } // 售票函數(shù) func saleTicket(name string) { for { // 在拿到共享資源之前先上鎖 mutex.Lock() if tickets > 0 { time.Sleep(time.Millisecond * 1) fmt.Println(name, "剩余票的數(shù)量為:", tickets) tickets-- } else { // 票賣(mài)完,解鎖 mutex.Unlock() fmt.Println("票已售完") break } // 操作完畢后,解鎖 mutex.Unlock() } }
上鎖之后,就不會(huì)出現(xiàn)問(wèn)題了
1.2 使用sync.WaitGroup等待一組Goroutine完成
sync.WaitGroup類(lèi)型可以用來(lái)等待一組Goroutine完成。例如:
package main import ( "fmt" "sync" "time" ) // waitgroup、 var wg sync.WaitGroup func main() { // 公司最后關(guān)門(mén)的人 0 // wg.Add(2) wg.Add(2)來(lái)告訴WaitGroup我們要等待兩個(gè)Goroutine完成 開(kāi)啟幾個(gè)協(xié)程,就add幾個(gè) // wg.Done() 我告知我已經(jīng)結(jié)束了 defer wg.Done()來(lái)在Goroutine完成時(shí)通知WaitGroup // 開(kāi)啟幾個(gè)協(xié)程,就add幾個(gè) wg.Add(2) go test1() go test2() fmt.Println("main等待ing") wg.Wait() // 等待 wg 歸零,wg.Wait()來(lái)等待所有Goroutine完成 代碼才會(huì)繼續(xù)向下執(zhí)行 fmt.Println("end") // 理想狀態(tài):所有協(xié)程執(zhí)行完畢之后,自動(dòng)停止。 //如果每次都強(qiáng)制設(shè)置個(gè)等待時(shí)間。那么協(xié)程代碼也可能在這個(gè)時(shí)間內(nèi)還沒(méi)跑完,也可能提前就跑完了,所以設(shè)置死的等待時(shí)間不合理。此時(shí)就需要用到了等待組WaitGroup //time.Sleep(1 * time.Second) } func test1() { for i := 0; i < 10; i++ { time.Sleep(1 * time.Second) fmt.Println("test1--", i) } wg.Done() //這里就將該代碼塊放在了其他邏輯之后 } func test2() { defer wg.Done() // defer wg.Done()來(lái)在Goroutine完成時(shí)通知WaitGroup 如果不用defer就得把該方法放在其他代碼之后 for i := 0; i < 10; i++ { fmt.Println("test2--", i) } }
主線程會(huì)等待所有協(xié)程執(zhí)行完畢,才繼續(xù)往下執(zhí)行代碼
1.3 注意事項(xiàng)
避免死鎖:確保在獲取鎖之后,無(wú)論發(fā)生什么情況(包括panic),都能夠釋放鎖??梢允褂胐efer語(yǔ)句來(lái)確保鎖的釋放。
減少鎖的持有時(shí)間:鎖的持有時(shí)間越長(zhǎng),其他goroutine被阻塞的時(shí)間就越長(zhǎng),系統(tǒng)的并發(fā)性能就越差。因此,應(yīng)該盡量減少鎖的持有時(shí)間,只在必要的代碼段中持有鎖。
避免嵌套鎖:盡量避免在一個(gè)鎖已經(jīng)持有的情況下再?lài)L試獲取另一個(gè)鎖,這可能會(huì)導(dǎo)致死鎖。
避免忘記調(diào)用Done:如果忘記調(diào)用Done方法,WaitGroup將會(huì)永遠(yuǎn)等待下去,導(dǎo)致程序無(wú)法正常結(jié)束。
避免負(fù)數(shù)計(jì)數(shù)器:調(diào)用Add方法時(shí),如果傳入的參數(shù)為負(fù)數(shù),或者導(dǎo)致計(jì)數(shù)器變?yōu)樨?fù)數(shù),將會(huì)導(dǎo)致panic。
二、讀寫(xiě)鎖(RWMutex)
讀寫(xiě)鎖(sync.RWMutex)允許多個(gè)goroutine同時(shí)讀取資源,但在寫(xiě)入時(shí)會(huì)阻塞所有其他讀和寫(xiě)的goroutine。讀寫(xiě)鎖可以提高讀多寫(xiě)少的場(chǎng)景下的并發(fā)性能。
2.1 基本用法
package main import ( "fmt" "sync" ) var ( data map[string]int rwMu sync.RWMutex ) func readData(key string) int { rwMu.RLock() defer rwMu.RUnlock() return data[key] } func writeData(key string, value int) { rwMu.Lock() defer rwMu.Unlock() data[key] = value } func main() { data = make(map[string]int) var wg sync.WaitGroup // 寫(xiě)操作 for i := 0; i < 10; i++ { wg.Add(1) go func(i int) { defer wg.Done() writeData(fmt.Sprintf("key%d", i), i*10) }(i) } // 讀操作 for i := 0; i < 100; i++ { wg.Add(1) go func(i int) { defer wg.Done() value := readData(fmt.Sprintf("key%d", i%10)) fmt.Printf("Read: key%d = %d\n", i%10, value) }(i) } wg.Wait() }
在上述代碼中,我們定義了一個(gè)全局變量data和一個(gè)讀寫(xiě)鎖rwMu。readData函數(shù)用于讀取data中的值,在讀取之前先獲取讀鎖,讀取完成后釋放讀鎖。
writeData函數(shù)用于寫(xiě)入data中的值,在寫(xiě)入之前先獲取寫(xiě)鎖,寫(xiě)入完成后釋放寫(xiě)鎖。
在main函數(shù)中,我們啟動(dòng)了10個(gè)寫(xiě)goroutine和100個(gè)讀goroutine,分別調(diào)用writeData和readData函數(shù)。通過(guò)sync.WaitGroup等待所有g(shù)oroutine完成。
2.2 注意事項(xiàng)
避免寫(xiě)鎖長(zhǎng)時(shí)間持有:寫(xiě)鎖會(huì)阻塞所有其他讀和寫(xiě)的goroutine,因此應(yīng)該盡量減少寫(xiě)鎖的持有時(shí)間。
讀多寫(xiě)少場(chǎng)景:讀寫(xiě)鎖適用于讀多寫(xiě)少的場(chǎng)景,如果寫(xiě)操作非常頻繁,讀寫(xiě)鎖的性能優(yōu)勢(shì)可能會(huì)消失。
避免嵌套鎖:與互斥鎖類(lèi)似,讀寫(xiě)鎖也應(yīng)該避免嵌套使用。
三、Once(一次執(zhí)行)
sync.Once用于確保某個(gè)操作只執(zhí)行一次,無(wú)論有多少個(gè)goroutine調(diào)用它。這對(duì)于單例模式或初始化只執(zhí)行一次的場(chǎng)景非常有用。
3.1 基本用法
package main import ( "fmt" "sync" ) var ( once sync.Once message string ) func initMessage() { message = "Hello, World!" } func printMessage() { once.Do(initMessage) fmt.Println(message) } func main() { var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() printMessage() }() } wg.Wait() }
在上述代碼中,我們定義了一個(gè)全局變量message和一個(gè)sync.Once類(lèi)型的變量once。
initMessage函數(shù)用于初始化message的值。printMessage函數(shù)通過(guò)once.Do方法確保initMessage只被調(diào)用一次,然后打印出message的值。
在main函數(shù)中,我們啟動(dòng)了10個(gè)goroutine,每個(gè)goroutine都調(diào)用printMessage函數(shù)。通過(guò)sync.WaitGroup等待所有g(shù)oroutine完成。
3.2 注意事項(xiàng)
避免重復(fù)初始化:sync.Once確保某個(gè)操作只執(zhí)行一次,因此它通常用于初始化全局變量或執(zhí)行其他只需要執(zhí)行一次的操作。
性能開(kāi)銷(xiāo):雖然sync.Once的性能開(kāi)銷(xiāo)很小,但在高性能要求的場(chǎng)景下,仍然需要注意其使用。
四、總結(jié)
本文詳細(xì)介紹了Go語(yǔ)言中sync包中的鎖機(jī)制,包括互斥鎖(sync.Mutex)、讀寫(xiě)鎖(sync.RWMutex)、Once(一次執(zhí)行)和WaitGroup(等待組)。
通過(guò)實(shí)際案例,幫助讀者理解和使用這些鎖。在并發(fā)編程中,正確地使用這些同步原語(yǔ),可以確保多個(gè)goroutine安全地訪問(wèn)共享資源,避免數(shù)據(jù)競(jìng)爭(zhēng)和其他并發(fā)問(wèn)題。希望本文能夠?qū)Υ蠹矣兴鶐椭?/p>
以上就是Go多線程中數(shù)據(jù)不一致問(wèn)題的解決方案的詳細(xì)內(nèi)容,更多關(guān)于Go多線程數(shù)據(jù)不一致的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang map如何生成有序的json數(shù)據(jù)詳解
最近在學(xué)習(xí)Golang,發(fā)現(xiàn)了一個(gè)問(wèn)題,覺(jué)著有必要給大家總結(jié)下,下面這篇文章主要給大家介紹了關(guān)于Golang map如何生成有序json數(shù)據(jù)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友們下面來(lái)一起看看吧。2017-07-07基于Golang實(shí)現(xiàn)統(tǒng)一加載資源的入口
當(dāng)我們需要在?main?函數(shù)中做一些初始化的工作,比如初始化日志,初始化配置文件,都需要統(tǒng)一初始化入口函數(shù),所以本文就來(lái)編寫(xiě)一個(gè)統(tǒng)一加載資源的入口吧2023-05-05golang-gin-mgo高并發(fā)服務(wù)器搭建教程
這篇文章主要介紹了golang-gin-mgo高并發(fā)服務(wù)器搭建教程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-12-12gorm golang 并發(fā)連接數(shù)據(jù)庫(kù)報(bào)錯(cuò)的解決方法
今天小編就為大家分享一篇gorm golang 并發(fā)連接數(shù)據(jù)庫(kù)報(bào)錯(cuò)的解決方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-07-07使用Go語(yǔ)言實(shí)現(xiàn)跨域資源共享(CORS)設(shè)置
在Web開(kāi)發(fā)中,跨域資源共享(CORS)是一種重要的安全機(jī)制,它允許許多資源在一個(gè)網(wǎng)頁(yè)上被另一個(gè)來(lái)源的網(wǎng)頁(yè)所訪問(wèn),然而,出于安全考慮,瀏覽器默認(rèn)禁止這種跨域訪問(wèn),為了解決這個(gè)問(wèn)題,我們可以使用Go語(yǔ)言來(lái)設(shè)置CORS,需要的朋友可以參考下2024-06-06