Golang并發(fā)繞不開的重要組件之Goroutine詳解
并發(fā)指的是程序由若干個獨立運行的代碼組成,主要依賴于多核CPU的并行運算和調度能力。
Golang在并發(fā)方面的能力較為突出,通過Goroutinue實現(xiàn)了典型的協(xié)程的概念。Golang的并發(fā)理念是:通過通信來共享內存,而不是通過共享內存來通信。
goroutinue與傳統(tǒng)線程的區(qū)別
主要體現(xiàn)在四個方面:
1.內存占用不同:Goroutinue的創(chuàng)建消耗2kb內存,并且在??臻g不足時會自動擴容;而線程默認會占用較大的??臻g(1-8MB),且??臻g大小不變,會有溢出風險
2.開銷不同:goroutinue創(chuàng)建和銷毀消耗都非常小,是用戶態(tài)線程;而線程的創(chuàng)建和銷毀都會有巨大的消耗,是內核級交互
3.調度切換不同:goroutinue切換消耗200ns,在1.4后優(yōu)化至20ns;而線程切換消耗1000-15000納秒
4.復雜性不同:goroutinue簡單易用,M個線程托管N個goroutinue;線程創(chuàng)建和退出復雜,多個線程間通訊復雜,使用網(wǎng)絡多路復用,應用服務線程門檻高
如果想實現(xiàn)一個并發(fā)程序,要考慮幾個方面:
程序代碼如何獨立運行?
獨立運行的代碼如何進行通信?
如何做到數(shù)據(jù)同步、調度同步?
這就引出了Golang并發(fā)編程中的幾個重要組件:Goroutinue、Channel、Context、Sync
Goroutine
Golang中并發(fā)執(zhí)行的單元稱為Goroutinue,也就是Go協(xié)程
使用方法非常簡單,使用go關鍵字即可啟動新的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í)行結果:
1 2 4 3 6 5 7 8 10 9
我們只需要一個go關鍵字就可以非常簡便的啟動一個Goroutinue協(xié)程。最后程序睡眠1秒,原因是主Goroutinue(main函數(shù))需要等待內部Goroutinue運行結束才能結束,否則子Goroutinue程序可能執(zhí)行一半會被強制停止。
調度的隨機性
通過結果可以看到數(shù)字的輸出順序并不是按照一定順序,因為Goroutinue的調度執(zhí)行是隨機的。
Goroutinue的并發(fā)規(guī)模
goroutinue本身的數(shù)量是無上限的,但是一定會受到棧內存空間以及操作系統(tǒng)的資源限制,可以通過函數(shù) runtime.NumGoroutine()獲取當前Goroutinue數(shù)量。前面也提到過,一個Goroutinue初始的棧內存只有2KB,用于保存Goroutinue中的執(zhí)行數(shù)據(jù),且棧內存可以擴容,按需增大或縮小,單個Goroutinue最大可以擴展到1GB。
上面通過time sleep的方法太傻了,我們可以通過官方提供的 sync.WaitGroup 來實現(xiàn)Goroutinue的協(xié)同調度。
sync.WaitGroup
sync.WaitGroup用于等待一組Goroutinue執(zhí)行完畢,其實是一個計數(shù)器思想的實現(xiàn)方案,它的核心方法有三個:
- Add():調用此函數(shù)用于增加等待的Goroutinue數(shù)量,原子操作保證并發(fā)安全
- Done():調用此函數(shù)用于減去一個計數(shù),原子操作保證并發(fā)安全
- Wait():調用此函數(shù)用于阻塞,直到所有的Goroutinue完成,也就是計數(shù)器歸0時,才會解除阻塞狀態(tài)
現(xiàn)在我們將上面的代碼最后一行的 time.Sleep(time.Second) 去掉,再次執(zhí)行會得到空的輸出,原因就是主Goroutinue直接結束,兩個子Goroutinue沒來得及執(zhí)行就已經退出了,讓我們用 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í)行結果:
waiting...
2 1 3 4 6 5 7 8 9 10
finish...
這個簡單的例子可以比較直觀的展示waitGroup的基礎用法。waitGroup 適用于一個主Goroutinue需要等待其他Goroutinue全部運行結束后才結束的這種場景,不適用于主Goroutinue需要結束,而通知其他Goroutinue結束的情景。
在這里有個使用上的注意事項,那就是 waitGroup 不要復制使用,因為內部維護的計數(shù)器不能修改,否則會造成Goroutinue的泄露,在傳值時需要用指針類型來進行傳遞。
waitGroup的內部結構
可以進入源碼查看內部結構:
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 }
可以看到并不是一個復雜的結構,其中含義:
- noCopy: 用于保證不會被復制
- state1: 以64bit計算機為例,高32bit是計數(shù)器
- state2: 以64bit計算機為例,低32bit是等待的Goroutinue
三大關鍵函數(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 } } }
到此這篇關于Golang并發(fā)繞不開的重要組件之Goroutine詳解的文章就介紹到這了,更多相關Golang Goroutine內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
- Golang 語言控制并發(fā) Goroutine的方法
- Go并發(fā)編程之goroutine使用正確方法
- Go并發(fā)的方法之goroutine模型與調度策略
- Go語言中的并發(fā)goroutine底層原理
- Go語言使用goroutine及通道實現(xiàn)并發(fā)詳解
- GoLang并發(fā)機制探究goroutine原理詳細講解
- Go中Goroutines輕量級并發(fā)的特性及效率探究
- 詳解Go語言中如何通過Goroutine實現(xiàn)高并發(fā)
- golang并發(fā)編程中Goroutine 協(xié)程的實現(xiàn)
- Go 并發(fā)編程Goroutine的實現(xiàn)示例
相關文章
golang sync.Pool 指針數(shù)據(jù)覆蓋問題解決
本文主要介紹了使用sync.Pool時遇到指針數(shù)據(jù)覆蓋的問題,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2025-03-03go開發(fā)alertmanger實現(xiàn)釘釘報警
本文主要介紹了go開發(fā)alertmanger實現(xiàn)釘釘報警,通過自己的url實現(xiàn)alertmanager的釘釘報警,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-07-07