習(xí)筆記03:goroutine、channel、WaitGroup 與 Mutex 并發(fā)入門)
上一篇主要整理了 Go 中函數(shù)、struct、指針和 interface 的基礎(chǔ)用法。這一篇繼續(xù)記錄 Go 中非常有特色的一部分并發(fā)編程。本篇主要整理目前已經(jīng)學(xué)習(xí)過的幾個(gè)知識(shí)點(diǎn)goroutine channel 緩沖 channel close WaitGroup defer Mutex select timeout這些內(nèi)容以后在 Go 后端、網(wǎng)絡(luò)編程以及并發(fā)任務(wù)處理中都會(huì)經(jīng)常遇到。一、goroutine 是什么goroutine 可以先簡(jiǎn)單理解成讓一個(gè)函數(shù)并發(fā)執(zhí)行。普通調(diào)用func work() { fmt.Println(working) } func main() { work() }這里work()會(huì)按照正常順序執(zhí)行。如果加上go work()就表示啟動(dòng)一個(gè)新的 goroutine 來執(zhí)行work()。例如package main import fmt func work() { fmt.Println(working) } func main() { go work() }需要注意如果main()太快結(jié)束新的 goroutine 可能還沒來得及執(zhí)行整個(gè)程序就已經(jīng)退出了。所以 goroutine 啟動(dòng)之后通常還需要配合其他機(jī)制進(jìn)行等待或同步。二、channel 是什么channel 可以理解成goroutine 之間傳遞數(shù)據(jù)的通道。創(chuàng)建一個(gè)傳遞整數(shù)的 channelch : make(chan int)發(fā)送數(shù)據(jù)ch - 10接收數(shù)據(jù)x : -ch例如package main import fmt func main() { ch : make(chan int) go func() { ch - 20 }() x : -ch fmt.Println(x) }輸出20這里ch - 20表示把20放進(jìn) channel。而x : -ch表示從 channel 取出一個(gè)值。三、channel 還可以用于等待如果x : -ch執(zhí)行時(shí) channel 里暫時(shí)沒有數(shù)據(jù)那么當(dāng)前 goroutine 會(huì)在這里等待。直到另一個(gè) goroutine執(zhí)行ch - 100程序才會(huì)繼續(xù)。所以 channel 不僅能傳遞數(shù)據(jù)還能起到一定的同步作用。四、無緩沖 channel下面這種ch : make(chan int)叫做無緩沖 channel。它的特點(diǎn)是發(fā)送和接收通常需要配合進(jìn)行。例如ch : make(chan int) ch - 10如果沒有其他 goroutine 來接收數(shù)據(jù)那么ch - 10可能會(huì)一直阻塞。正確示例ch : make(chan int) go func() { ch - 10 }() fmt.Println(-ch)輸出10五、有緩沖 channel還可以創(chuàng)建有緩沖的 channelch : make(chan int, 2)這里的2表示 channel 最多可以暫時(shí)存放 2 個(gè)整數(shù)。例如ch : make(chan int, 2) ch - 10 ch - 20 fmt.Println(-ch) fmt.Println(-ch)輸出10 20可以先理解為[10, 20]先放進(jìn)去的值先被取出來。如果緩沖區(qū)已經(jīng)滿了再繼續(xù)發(fā)送數(shù)據(jù)就會(huì)阻塞直到有人先取走數(shù)據(jù)。六、關(guān)閉 channel可以使用close(ch)關(guān)閉 channel。需要注意關(guān)閉 channel 并不會(huì)立即清空里面已經(jīng)存在的數(shù)據(jù)。例如ch : make(chan int, 2) ch - 10 ch - 20 close(ch) fmt.Println(-ch) fmt.Println(-ch)仍然可以得到10 20close(ch)更準(zhǔn)確的意思是告訴接收方以后不會(huì)再發(fā)送新的數(shù)據(jù)了。七、判斷 channel 是否已經(jīng)讀取完可以這樣接收value, ok : -ch其中value - 讀到的數(shù)據(jù) ok - 是否還能正常讀取例如ch : make(chan int, 1) ch - 8 close(ch) value, ok : -ch此時(shí)value 8 ok true如果數(shù)據(jù)已經(jīng)全部讀取完再讀取一次value 0 ok false八、使用 range 讀取 channelchannel 也可以配合rangefor value : range ch { fmt.Println(value) }例如ch : make(chan int, 3) ch - 1 ch - 2 ch - 3 close(ch) for value : range ch { fmt.Println(value) }輸出1 2 3這個(gè)循環(huán)會(huì)一直讀取直到channel 已經(jīng)關(guān)閉并且里面的數(shù)據(jù)全部讀取完。所以如果使用for value : range ch一般要注意 channel 是否會(huì)被關(guān)閉否則循環(huán)可能一直等待。九、WaitGroup 是什么如果啟動(dòng)了多個(gè) goroutine有時(shí)希望主程序等它們?nèi)繄?zhí)行結(jié)束。這時(shí)候可以使用sync.WaitGroup例如var wg sync.WaitGroup常見三個(gè)操作wg.Add(1) wg.Done() wg.Wait()可以理解為Add(n) - 登記 n 個(gè)任務(wù) Done() - 當(dāng)前任務(wù)完成 Wait() - 等待所有任務(wù)完成例如package main import ( fmt sync ) func main() { var wg sync.WaitGroup wg.Add(2) go func() { fmt.Println(任務(wù)1) wg.Done() }() go func() { fmt.Println(任務(wù)2) wg.Done() }() wg.Wait() fmt.Println(全部完成) }任務(wù)1和任務(wù)2的先后順序不一定固定。但全部完成一定會(huì)在兩個(gè)任務(wù)都結(jié)束之后輸出。十、defer 的基礎(chǔ)用法defer可以先理解成把某個(gè)操作延遲到當(dāng)前函數(shù)結(jié)束之前執(zhí)行。例如func test() { defer fmt.Println(B) fmt.Println(A) }輸出A B因?yàn)閐efer fmt.Println(B)不會(huì)馬上執(zhí)行而是在函數(shù)結(jié)束之前執(zhí)行。十一、多個(gè) defer 的執(zhí)行順序如果有多個(gè)deferfunc test() { defer fmt.Println(A) defer fmt.Println(B) defer fmt.Println(C) fmt.Println(D) }輸出D C B A多個(gè)defer是后進(jìn)先出。也就是最后登記的defer最先執(zhí)行。十二、defer 和 WaitGroup 配合實(shí)際代碼里常寫go func() { defer wg.Done() fmt.Println(working) }()這樣可以保證當(dāng)前 goroutine 結(jié)束時(shí)執(zhí)行wg.Done()相比把wg.Done()手動(dòng)放在函數(shù)最后更不容易忘記。十三、Mutex 是什么多個(gè) goroutine 同時(shí)修改同一個(gè)變量時(shí)可能出現(xiàn)數(shù)據(jù)競(jìng)爭(zhēng)。例如count看起來只有一行但實(shí)際可以理解成讀取 count 加 1 寫回 count如果多個(gè) goroutine 同時(shí)進(jìn)行就可能導(dǎo)致結(jié)果不準(zhǔn)確。這時(shí)候可以使用sync.Mutex也就是互斥鎖。基本寫法mu.Lock() count mu.Unlock()可以理解為Lock() - 我現(xiàn)在開始修改其他 goroutine 先等一下 Unlock() - 我修改完成了其他 goroutine 可以繼續(xù)十四、WaitGroup 和 Mutex 綜合使用例如啟動(dòng) 5 個(gè) goroutine每個(gè)都讓count加 1package main import ( fmt sync ) func main() { var wg sync.WaitGroup var mu sync.Mutex count : 0 for i : 0; i 5; i { wg.Add(1) go func() { defer wg.Done() mu.Lock() count mu.Unlock() }() } wg.Wait() fmt.Println(count) }最終輸出5這里WaitGroup負(fù)責(zé)等待 5 個(gè) goroutine 全部完成。而Mutex負(fù)責(zé)保證count不會(huì)被多個(gè) goroutine 同時(shí)修改。十五、goroutine 把結(jié)果通過 channel 返回例如func square(n int, ch chan int) { ch - n * n }調(diào)用ch : make(chan int) go square(5, ch) result : -ch fmt.Println(result)輸出25這里square()在新的 goroutine 中進(jìn)行計(jì)算。然后通過ch - n * n把結(jié)果傳回主 goroutine。十六、并發(fā)任務(wù)完成順序不固定例如go square(2, ch) go square(3, ch) a : -ch b : -ch最終得到的兩個(gè)結(jié)果一定是4 9但a 4, b 9和a 9, b 4都有可能。原因是goroutine 的啟動(dòng)順序不代表它們一定按照這個(gè)順序完成。這是學(xué)習(xí)并發(fā)時(shí)比較重要的一點(diǎn)。十七、select 是什么select用來處理多個(gè) channel。例如select { case msg1 : -ch1: fmt.Println(msg1) case msg2 : -ch2: fmt.Println(msg2) }可以理解為同時(shí)等待多個(gè) channel哪個(gè)先準(zhǔn)備好就先處理哪個(gè)。如果ch1先有數(shù)據(jù)就執(zhí)行第一個(gè)case。如果ch2先有數(shù)據(jù)就執(zhí)行第二個(gè)。十八、select 中的 default例如select { case value : -ch: fmt.Println(value) default: fmt.Println(no data) }如果當(dāng)前 channel 沒有數(shù)據(jù)那么不會(huì)一直等待而是直接執(zhí)行no data所以default可以理解為如果當(dāng)前沒有任何 case 可以立即執(zhí)行就執(zhí)行 default。需要注意default不是最后一定會(huì)執(zhí)行。只要有其他case可以執(zhí)行就不會(huì)進(jìn)入default。十九、select timeout 超時(shí)控制實(shí)際開發(fā)中經(jīng)常不能一直等待某個(gè)任務(wù)。這時(shí)可以使用time.After()例如select { case msg : -ch: fmt.Println(msg) case -time.After(2 * time.Second): fmt.Println(timeout) }意思是等待 channel 的數(shù)據(jù)但最多等待 2 秒。如果 2 秒內(nèi)收到數(shù)據(jù)就處理數(shù)據(jù)。如果 2 秒仍然沒有收到就輸出timeout二十、超時(shí)控制完整例子package main import ( fmt time ) func worker(ch chan string) { time.Sleep(1 * time.Second) ch - 任務(wù)完成 } func main() { ch : make(chan string) go worker(ch) select { case msg : -ch: fmt.Println(msg) case -time.After(2 * time.Second): fmt.Println(timeout) } }因?yàn)槿蝿?wù)只需要等待 1 秒而超時(shí)時(shí)間是 2 秒所以最終輸出任務(wù)完成如果把time.Sleep(1 * time.Second)改成time.Sleep(3 * time.Second)那么超時(shí)時(shí)間會(huì)先到最終輸出timeout二十一、目前容易混淆的幾個(gè)地方1. goroutine 和 channel 不是同一個(gè)東西goroutine 是執(zhí)行任務(wù)的并發(fā)單元。channel 是goroutine 之間傳遞數(shù)據(jù)的通道。可以簡(jiǎn)單理解成goroutine - 干活的人 channel - 傳遞數(shù)據(jù)的通道2. goroutine 不是進(jìn)程一開始容易把go func()理解成“創(chuàng)建新進(jìn)程”。實(shí)際上更準(zhǔn)確的說法是啟動(dòng)新的 goroutine。goroutine 是 Go 運(yùn)行時(shí)管理的輕量級(jí)并發(fā)任務(wù)并不是操作系統(tǒng)進(jìn)程。3. 啟動(dòng)順序不等于完成順序例如go task1() go task2()并不能保證task1 一定先完成 task2 一定后完成這也是并發(fā)程序需要特別注意的地方。4. WaitGroup 和 Mutex 作用不同WaitGroup等待多個(gè) goroutine 全部完成。Mutex防止多個(gè) goroutine 同時(shí)修改共享數(shù)據(jù)。它們解決的是兩個(gè)不同的問題。二十二、學(xué)習(xí)總結(jié)這一階段主要學(xué)習(xí)了 Go 的并發(fā)基礎(chǔ)。目前我對(duì)幾個(gè)核心概念的理解是goroutine - 并發(fā)執(zhí)行任務(wù)channel - goroutine 之間傳遞數(shù)據(jù)WaitGroup - 等待多個(gè) goroutine 完成Mutex - 保護(hù)共享數(shù)據(jù)避免數(shù)據(jù)競(jìng)爭(zhēng)defer - 函數(shù)結(jié)束前執(zhí)行指定操作select - 同時(shí)處理多個(gè) channeltime.After - 實(shí)現(xiàn)簡(jiǎn)單的超時(shí)控制Go 的并發(fā)部分比前面的基礎(chǔ)語法更抽象目前還只是掌握了基本概念和簡(jiǎn)單用法。后面還需要通過實(shí)際項(xiàng)目繼續(xù)鞏固。下一階段準(zhǔn)備開始學(xué)習(xí) Go 的工程基礎(chǔ)包括package go mod 項(xiàng)目目錄結(jié)構(gòu) JSON 文件讀寫 go test之后再進(jìn)入 HTTP、Gin 和 MySQL開始真正編寫 Go 后端項(xiàng)目。