Golang并發(fā)繞不開的重要組件之Goroutine詳解
并發(fā)指的是程序由若干個(gè)獨(dú)立運(yùn)行的代碼組成,主要依賴于多核CPU的并行運(yùn)算和調(diào)度能力。
Golang在并發(fā)方面的能力較為突出,通過Goroutinue實(shí)現(xiàn)了典型的協(xié)程的概念。Golang的并發(fā)理念是:通過通信來共享內(nèi)存,而不是通過共享內(nèi)存來通信。
goroutinue與傳統(tǒng)線程的區(qū)別
主要體現(xiàn)在四個(gè)方面:
1.內(nèi)存占用不同:Goroutinue的創(chuàng)建消耗2kb內(nèi)存,并且在??臻g不足時(shí)會自動(dòng)擴(kuò)容;而線程默認(rèn)會占用較大的??臻g(1-8MB),且棧空間大小不變,會有溢出風(fēng)險(xiǎn)
2.開銷不同:goroutinue創(chuàng)建和銷毀消耗都非常小,是用戶態(tài)線程;而線程的創(chuàng)建和銷毀都會有巨大的消耗,是內(nèi)核級交互
3.調(diào)度切換不同:goroutinue切換消耗200ns,在1.4后優(yōu)化至20ns;而線程切換消耗1000-15000納秒
4.復(fù)雜性不同:goroutinue簡單易用,M個(gè)線程托管N個(gè)goroutinue;線程創(chuàng)建和退出復(fù)雜,多個(gè)線程間通訊復(fù)雜,使用網(wǎng)絡(luò)多路復(fù)用,應(yīng)用服務(wù)線程門檻高
如果想實(shí)現(xiàn)一個(gè)并發(fā)程序,要考慮幾個(gè)方面:
程序代碼如何獨(dú)立運(yùn)行?
獨(dú)立運(yùn)行的代碼如何進(jìn)行通信?
如何做到數(shù)據(jù)同步、調(diào)度同步?
這就引出了Golang并發(fā)編程中的幾個(gè)重要組件:Goroutinue、Channel、Context、Sync
Goroutine
Golang中并發(fā)執(zhí)行的單元稱為Goroutinue,也就是Go協(xié)程
使用方法非常簡單,使用go關(guān)鍵字即可啟動(dòng)新的Goroutinue
示例代碼:
func main() { // 輸出奇數(shù) printOdd := func() { for i := 1; i <= 10; i += 2 { fmt.Println(i) time.Sleep(100 * time.Millisecond) } } // 輸出偶數(shù) printEven := func() { for i := 2; i <= 10; i += 2 { fmt.Println(i) time.Sleep(100 * time.Millisecond) } } go printOdd() go printEven() // 阻塞等待 time.Sleep(time.Second) }
執(zhí)行結(jié)果:
1 2 4 3 6 5 7 8 10 9
我們只需要一個(gè)go關(guān)鍵字就可以非常簡便的啟動(dòng)一個(gè)Goroutinue協(xié)程。最后程序睡眠1秒,原因是主Goroutinue(main函數(shù))需要等待內(nèi)部Goroutinue運(yùn)行結(jié)束才能結(jié)束,否則子Goroutinue程序可能執(zhí)行一半會被強(qiáng)制停止。
調(diào)度的隨機(jī)性
通過結(jié)果可以看到數(shù)字的輸出順序并不是按照一定順序,因?yàn)镚oroutinue的調(diào)度執(zhí)行是隨機(jī)的。
Goroutinue的并發(fā)規(guī)模
goroutinue本身的數(shù)量是無上限的,但是一定會受到棧內(nèi)存空間以及操作系統(tǒng)的資源限制,可以通過函數(shù) runtime.NumGoroutine()獲取當(dāng)前Goroutinue數(shù)量。前面也提到過,一個(gè)Goroutinue初始的棧內(nèi)存只有2KB,用于保存Goroutinue中的執(zhí)行數(shù)據(jù),且棧內(nèi)存可以擴(kuò)容,按需增大或縮小,單個(gè)Goroutinue最大可以擴(kuò)展到1GB。
上面通過time sleep的方法太傻了,我們可以通過官方提供的 sync.WaitGroup 來實(shí)現(xiàn)Goroutinue的協(xié)同調(diào)度。
sync.WaitGroup
sync.WaitGroup用于等待一組Goroutinue執(zhí)行完畢,其實(shí)是一個(gè)計(jì)數(shù)器思想的實(shí)現(xiàn)方案,它的核心方法有三個(gè):
- Add():調(diào)用此函數(shù)用于增加等待的Goroutinue數(shù)量,原子操作保證并發(fā)安全
- Done():調(diào)用此函數(shù)用于減去一個(gè)計(jì)數(shù),原子操作保證并發(fā)安全
- Wait():調(diào)用此函數(shù)用于阻塞,直到所有的Goroutinue完成,也就是計(jì)數(shù)器歸0時(shí),才會解除阻塞狀態(tài)
現(xiàn)在我們將上面的代碼最后一行的 time.Sleep(time.Second) 去掉,再次執(zhí)行會得到空的輸出,原因就是主Goroutinue直接結(jié)束,兩個(gè)子Goroutinue沒來得及執(zhí)行就已經(jīng)退出了,讓我們用 WaitGroup 來改造一下
示例代碼:
func main() { wg := sync.WaitGroup{} // 輸出奇數(shù) printOdd := func() { defer wg.Done() for i := 1; i <= 10; i += 2 { fmt.Printf("%d ", i) time.Sleep(100 * time.Millisecond) } } // 輸出偶數(shù) printEven := func() { defer wg.Done() for i := 2; i <= 10; i += 2 { fmt.Printf("%d ", i) time.Sleep(100 * time.Millisecond) } } wg.Add(2) go printOdd() go printEven() // 阻塞等待 fmt.Println("waiting...") wg.Wait() fmt.Println("\nfinish...") }
執(zhí)行結(jié)果:
waiting...
2 1 3 4 6 5 7 8 9 10
finish...
這個(gè)簡單的例子可以比較直觀的展示waitGroup的基礎(chǔ)用法。waitGroup 適用于一個(gè)主Goroutinue需要等待其他Goroutinue全部運(yùn)行結(jié)束后才結(jié)束的這種場景,不適用于主Goroutinue需要結(jié)束,而通知其他Goroutinue結(jié)束的情景。
在這里有個(gè)使用上的注意事項(xiàng),那就是 waitGroup 不要復(fù)制使用,因?yàn)閮?nèi)部維護(hù)的計(jì)數(shù)器不能修改,否則會造成Goroutinue的泄露,在傳值時(shí)需要用指針類型來進(jìn)行傳遞。
waitGroup的內(nèi)部結(jié)構(gòu)
可以進(jìn)入源碼查看內(nèi)部結(jié)構(gòu):
type WaitGroup struct { noCopy noCopy // 64-bit value: high 32 bits are counter, low 32 bits are waiter count. // 64-bit atomic operations require 64-bit alignment, but 32-bit // compilers only guarantee that 64-bit fields are 32-bit aligned. // For this reason on 32 bit architectures we need to check in state() // if state1 is aligned or not, and dynamically "swap" the field order if // needed. state1 uint64 state2 uint32 }
可以看到并不是一個(gè)復(fù)雜的結(jié)構(gòu),其中含義:
- noCopy: 用于保證不會被復(fù)制
- state1: 以64bit計(jì)算機(jī)為例,高32bit是計(jì)數(shù)器
- state2: 以64bit計(jì)算機(jī)為例,低32bit是等待的Goroutinue
三大關(guān)鍵函數(shù)核心代碼:
func (wg *WaitGroup) Add(delta int) { ... state := atomic.AddUint64(statep, uint64(delta)<<32) ... } func (wg *WaitGroup) Done() { wg.Add(-1) } func (wg *WaitGroup) Wait() { ... for { state := atomic.LoadUint64(statep) v := int32(state >> 32) w := uint32(state) if v == 0 { // Counter is 0, no need to wait. if race.Enabled { race.Enable() race.Acquire(unsafe.Pointer(wg)) } return } // Increment waiters count. if atomic.CompareAndSwapUint64(statep, state, state+1) { if race.Enabled && w == 0 { // Wait must be synchronized with the first Add. // Need to model this is as a write to race with the read in Add. // As a consequence, can do the write only for the first waiter, // otherwise concurrent Waits will race with each other. race.Write(unsafe.Pointer(semap)) } runtime_Semacquire(semap) if *statep != 0 { panic("sync: WaitGroup is reused before previous Wait has returned") } if race.Enabled { race.Enable() race.Acquire(unsafe.Pointer(wg)) } return } } }
到此這篇關(guān)于Golang并發(fā)繞不開的重要組件之Goroutine詳解的文章就介紹到這了,更多相關(guān)Golang Goroutine內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Golang 語言控制并發(fā) Goroutine的方法
- Go并發(fā)編程之goroutine使用正確方法
- Go并發(fā)的方法之goroutine模型與調(diào)度策略
- Go語言中的并發(fā)goroutine底層原理
- Go語言使用goroutine及通道實(shí)現(xiàn)并發(fā)詳解
- GoLang并發(fā)機(jī)制探究goroutine原理詳細(xì)講解
- Go中Goroutines輕量級并發(fā)的特性及效率探究
- 詳解Go語言中如何通過Goroutine實(shí)現(xiàn)高并發(fā)
- golang并發(fā)編程中Goroutine 協(xié)程的實(shí)現(xiàn)
- Go 并發(fā)編程Goroutine的實(shí)現(xiàn)示例
相關(guān)文章
golang sync.Pool 指針數(shù)據(jù)覆蓋問題解決
本文主要介紹了使用sync.Pool時(shí)遇到指針數(shù)據(jù)覆蓋的問題,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-03-03Go語言使用時(shí)會遇到的錯(cuò)誤及解決方法詳解
這篇文章主要為大家詳細(xì)介紹了Go語言使用時(shí)常常會遇到的一些錯(cuò)誤及解決方法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2023-07-07go開發(fā)alertmanger實(shí)現(xiàn)釘釘報(bào)警
本文主要介紹了go開發(fā)alertmanger實(shí)現(xiàn)釘釘報(bào)警,通過自己的url實(shí)現(xiàn)alertmanager的釘釘報(bào)警,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-07-07重學(xué)Go語言之如何開發(fā)RPC應(yīng)用
這篇文章主要為大家詳細(xì)介紹了在Go語言中如何構(gòu)建RPC應(yīng)用,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-09-09