我們都知道,在并發(fā)編程中,線程安全是非常重要的。接下來(lái)我們就假定一個(gè)場(chǎng)景,復(fù)現(xiàn)一下線程不安全的情況,再聊聊如何在Go中解決
我們現(xiàn)在需要對(duì)1~100求他們的階乘,并將結(jié)果放到一個(gè)map中
1! = 1 = 12! = 1 * 2 = 23! = 1 * 2 * 3 = 64! = 1 * 2 * 3 * 4 = 245! = 1 * 2 * 3 * 4 * 5 = 120...{ 1: 1 2: 2 3: 6 4: 24 5: 120 ...}var factorialMap = make(map[int]int)func Factorial(n int) { result := 1 for i := 1; i <= n; i++ { result *= i } factorialMap[n] = result}func main() { for i := 1; i < 10; i++ { Factorial(i) } for k, v := range factorialMap { fmt.Printf("%d 的階乘是%d\n", k, v) }}上述代碼執(zhí)行結(jié)果其實(shí)是沒(méi)問(wèn)題的,為什么會(huì)出現(xiàn)亂序呢?因?yàn)檫@是go語(yǔ)言中map其實(shí)就是亂序的,按照我們的理解,先存的先出,但是不好意思,Golang的map不是這樣的。上面執(zhí)行也沒(méi)什么問(wèn)題啊,細(xì)心的同學(xué)可能發(fā)現(xiàn)了,這個(gè)版本的代碼并沒(méi)有用上并發(fā),對(duì)吧。好接下來(lái)我們繼續(xù)改進(jìn)
【資料圖】
var factorialMap = make(map[int]int)func Factorial(n int) { result := 1 for i := 1; i <= n; i++ { result *= i } factorialMap[n] = result}func main() { for i := 1; i < 10; i++ { go Factorial(i) } for k, v := range factorialMap { fmt.Printf("%d 的階乘是%d\n", k, v) }}我們可以發(fā)現(xiàn),并發(fā)版就是在調(diào)用計(jì)算階乘函數(shù)的前面加上了一個(gè)go而已。不要小看這個(gè)go,扯遠(yuǎn)了,當(dāng)然大家知道這是go語(yǔ)言中開(kāi)啟一個(gè)協(xié)程的關(guān)鍵字即可。
執(zhí)行結(jié)果就是,控制臺(tái)啥都沒(méi)輸出,這是因?yàn)橹鲄f(xié)程和子協(xié)程之間的執(zhí)行關(guān)系,下面我們畫(huà)圖理解
從上圖中我們可以發(fā)現(xiàn),主協(xié)程執(zhí)行的時(shí)間短(表現(xiàn)在比較短),子協(xié)程執(zhí)行時(shí)間比較長(zhǎng)(表現(xiàn)在比較長(zhǎng))我們一定要記住,子協(xié)程是相對(duì)于當(dāng)前的主協(xié)程來(lái)說(shuō)的,如果主協(xié)程不存在了,那就沒(méi)有子協(xié)程了
所以上面代碼啥都沒(méi)輸出就是因?yàn)?,主協(xié)程已經(jīng)執(zhí)行完了,但是子協(xié)程還沒(méi)做完,那子協(xié)程都沒(méi)做完,factorialMap中能有東西嗎?
這就引出我們第一個(gè)問(wèn)題,主協(xié)程如何等待子協(xié)程執(zhí)行完再退出程序。我們現(xiàn)在用一個(gè)最簡(jiǎn)單,最容易想到的做法
var factorialMap = make(map[int]int)func Factorial(n int) { result := 1 for i := 1; i <= n; i++ { result *= i } factorialMap[n] = result}func main() { for i := 1; i < 100; i++ { go Factorial(i) } time.Sleep(time.Second * 3) for k, v := range factorialMap { fmt.Printf("%d 的階乘是%d\n", k, v) }}當(dāng)并發(fā)數(shù)比較小的時(shí)候,這個(gè)問(wèn)題可能不會(huì)出現(xiàn),一旦并發(fā)數(shù)變大,問(wèn)題就立馬出現(xiàn)了
圖中的執(zhí)行結(jié)果是并發(fā)map寫(xiě)入錯(cuò)誤為什么會(huì)出現(xiàn)這個(gè)問(wèn)題,我們假設(shè)100個(gè)人往一個(gè)籃子里放水果,很容易。但是100個(gè)人從一個(gè)籃子里拿水果,那就會(huì)出問(wèn)題,首先,籃子里的水果不一定夠100個(gè),其二每個(gè)人都想先拿,必然會(huì)引起爭(zhēng)搶。
針對(duì)上面的問(wèn)題,我們引入全局鎖的概念。這就有點(diǎn)像我們上廁所,100個(gè)人都想上廁所,但廁所只有1個(gè),誰(shuí)先搶到了誰(shuí)先上,并且這個(gè)人還有給廁所上鎖,防止其他人進(jìn)來(lái)
var factorialMap = make(map[int]int)var lock sync.Mutexfunc Factorial(n int) { result := 1 for i := 1; i <= n; i++ { result *= i } // defer 不好理解 // defer func(){ // lock.Unlock() // 執(zhí)行完解鎖 // }() lock.Lock() // 執(zhí)行時(shí)上鎖 factorialMap[n] = result lock.Unlock() // 執(zhí)行后解鎖}func main() { for i := 1; i < 100; i++ { go Factorial(i) } time.Sleep(time.Second * 3) for k, v := range factorialMap { fmt.Printf("%d 的階乘是%d\n", k, v) }}執(zhí)行結(jié)果有0可能是數(shù)據(jù)類(lèi)型存不下了導(dǎo)致的,這個(gè)大家不用關(guān)心
這樣我們就解決了資源競(jìng)爭(zhēng)的問(wèn)題了。但其實(shí)還有一個(gè)問(wèn)題,就是我們?cè)谥鲄f(xié)程中還是必須手動(dòng)等待,這要非常不好,那如果子協(xié)程3秒內(nèi)解決不了怎么辦?
這個(gè)問(wèn)題是我們不想在主協(xié)程中手動(dòng)等待子協(xié)程,換句話說(shuō)是我們不想直接在代碼中寫(xiě)明要等待多長(zhǎng)時(shí)間
這里我們就引入了WaitGroup
var factorialMap = make(map[int]int)var lock sync.Mutexvar wg sync.WaitGroupfunc Factorial(n int) { result := 1 for i := 1; i <= n; i++ { result *= i } lock.Lock() // 執(zhí)行時(shí)上鎖 factorialMap[n] = result lock.Unlock() // 執(zhí)行后解鎖 wg.Done()}func main() { for i := 1; i < 100; i++ { wg.Add(1) go Factorial(i) } wg.Wait() for k, v := range factorialMap { fmt.Printf("%d 的階乘是%d\n", k, v) }}WaitGroup的內(nèi)部原理大家自己細(xì)扣,我這就不講了總結(jié)來(lái)說(shuō)就是WaitGroup是一個(gè)籃子,每開(kāi)一個(gè)協(xié)程,就往籃子中加一個(gè)標(biāo)識(shí)(Add函數(shù)),每執(zhí)行完一個(gè)協(xié)程,就從籃子中減去一個(gè)標(biāo)識(shí)(Done函數(shù)),最后查看籃子中,如果是空的,就表示協(xié)程執(zhí)行完了(Wait函數(shù))
【推薦學(xué)習(xí):go視頻教程】
以上就是一文聊聊Go語(yǔ)言中資源競(jìng)爭(zhēng)問(wèn)題的詳細(xì)內(nèi)容,更多請(qǐng)關(guān)注php中文網(wǎng)其它相關(guān)文章!