Go?并發(fā)編程協(xié)程及調(diào)度機(jī)制詳情
前言:
協(xié)程(coroutine)是 Go 語言最大的特色之一,goroutine 的實(shí)現(xiàn)其實(shí)是通過協(xié)程。
協(xié)程的概念
協(xié)程一詞最早出現(xiàn)在 1963 年發(fā)表的論文中,該論文的作者為美國(guó)計(jì)算機(jī)科學(xué)家 Melvin E.Conway。著名的康威定律:“設(shè)計(jì)系統(tǒng)的架構(gòu)受制于產(chǎn)生這些設(shè)計(jì)的組織的溝通結(jié)構(gòu)。” 也是這個(gè)作者。
協(xié)程是一種用戶態(tài)的輕量級(jí)線程,可以想成一個(gè)線程里面可以有多個(gè)協(xié)程,而協(xié)程的調(diào)度完全由用戶控制,協(xié)程也會(huì)有自己的 registers、context、stack 等等,并且由協(xié)程的調(diào)度器來控制說目前由哪個(gè)協(xié)程執(zhí)行,哪個(gè)協(xié)程要被 block 住。
而相對(duì)于 Thread 及 Process 的調(diào)度,則是由 CPU 內(nèi)核去進(jìn)行調(diào)度,因此操作系統(tǒng)其實(shí)會(huì)有所謂許多的調(diào)度算法,并且可以進(jìn)行搶占式調(diào)度,可以主動(dòng)搶奪執(zhí)行的控制權(quán)。
反之,協(xié)程是不行的,只能進(jìn)行非搶占式的調(diào)度。 可以理解成,如果 coroutine 被 block 住,則會(huì)在用戶態(tài)直接切換另外一個(gè) coroutine 給此 thread 繼續(xù)執(zhí)行,這樣其他 coroutine 就不會(huì)被 block 住,讓資源能夠有效的被利用,借此實(shí)現(xiàn) Concurrent 的概念。
協(xié)程與線程
線程 是 CPU 調(diào)度的基本單位,多個(gè)線程可以通過共享進(jìn)程的資源,通過共享內(nèi)存等方式來進(jìn)行線程間通信。
協(xié)程 可理解為輕量級(jí)線程,與線程相比,協(xié)程不受操作系統(tǒng)系統(tǒng)調(diào)度,協(xié)程調(diào)度由用戶應(yīng)用程序提供,協(xié)程調(diào)度器按照調(diào)度策略把協(xié)程調(diào)度到線程中運(yùn)行。
- 協(xié)程只需花幾 KB 就可以被創(chuàng)立,線程則需要幾 MB 的內(nèi)存才能創(chuàng)立
- 切換開銷方面,協(xié)程遠(yuǎn)遠(yuǎn)低于線程,切換的速度也因此大幅提升
goroutine 的誕生
Golang 語言的 goroutine 其實(shí)就是協(xié)程,特別的是在語言層面直接原生支持創(chuàng)立協(xié)程,并在 runtime、系統(tǒng)調(diào)用等多方面對(duì) goroutine 調(diào)度進(jìn)行封裝及處理。
相對(duì)于 Java 的建立線程,操作系統(tǒng)是會(huì)直接建立一個(gè)線程與其對(duì)應(yīng),而多個(gè)線程的間互相切換需要通過內(nèi)核線程來進(jìn)行,會(huì)有較大的上下文切換開銷,造成的資源耗費(fèi),而 goroutine 是在代碼上直接實(shí)現(xiàn)切換,不需要經(jīng)過內(nèi)核線程。
goroutine 的優(yōu)勢(shì):
- 與線程相比, goroutine 非常便宜,可以根據(jù)應(yīng)用程序的需求自動(dòng)分配, 但在線程的大小通常是固定的
- 使用 goroutine 訪問共享內(nèi)存的時(shí)候 透過 channel 可以避免競(jìng)態(tài)條件的發(fā)生
比如,我們計(jì)算一個(gè)數(shù)字的質(zhì)數(shù),可以寫出如下的代碼:
package main import ( "fmt" "time" ) func main() { num := 300000 start := time.Now() for i := 1; i <= num; i++ { if isPrime(i) { fmt.Println(i) } } end := time.Now() fmt.Println(end.Unix()-start.Unix(), "seconds") } func isPrime(num int) bool { if num == 1 { return false } else if num == 2 { return true } else { for i := 2; i < num; i++ { if num%i == 0 { return false } } return true } }
上面的代碼用 num := 300000 來測(cè)試,也就是從 1~300000 之間來看那些數(shù)字會(huì)是質(zhì)數(shù),如果是質(zhì)數(shù)的話就把質(zhì)數(shù)輸出,最后看到最終花費(fèi)了 37 秒。運(yùn)行結(jié)果如下:
使用 goroutine 加快速度
package main import ( "fmt" "time" ) func main() { num := 300000 start := time.Now() for i := 1; i <= num; i++ { go findPrimes(i) } end := time.Now() time.Sleep(5 * time.Second) fmt.Println(end.Unix()-start.Unix(), "seconds") } func findPrimes(num int) { if num == 1 { return } else if num == 2 { fmt.Println(num) } else { for i := 2; i < num; i++ { if num%i == 0 { return } } fmt.Println(num) } }
go findPrimes
這條語句就可以開啟一個(gè) goroutine,因此以主程序來說這樣等于是開啟 300000 個(gè) goroutine 來各自判斷自己拿到 num 是不是質(zhì)數(shù)這樣。
用 time. Sleep
來休息五秒來讓 main 主程序不要被關(guān)閉,否則由于開啟 goroutine 之后代碼會(huì)繼續(xù)往下執(zhí)行,如果沒做 sleep 的話會(huì)導(dǎo)致主程序關(guān)閉,主程序一關(guān)閉 goroutine 就跟著關(guān)閉了,我們就看不出效果了。
這邊運(yùn)行之后會(huì)發(fā)現(xiàn)輸出的質(zhì)數(shù)出現(xiàn)并不是從小到大的,這是因?yàn)檫@些 goroutine 是一起做事情的,所以誰先做完誰就先輸出這樣。
運(yùn)行結(jié)果如下,最后花費(fèi)了大概 11 秒:
goroutine 的機(jī)制原理
理解 goroutine 機(jī)制的原理,關(guān)鍵是理解 Go 語言是如何實(shí)現(xiàn)調(diào)度器模型的。
計(jì)算機(jī)科學(xué)領(lǐng)域的任何問題都可以通過添加間接中間層來解決。GPM 模型就是這一理論的實(shí)踐者。
Go 語言中支撐整個(gè)調(diào)度器實(shí)現(xiàn)的主要有 4 個(gè)重要結(jié)構(gòu),分別是 machine(簡(jiǎn)稱 M )、goroutine(簡(jiǎn)稱 G )、processor(簡(jiǎn)稱 P )、Scheduler(簡(jiǎn)稱 Sched), 前三個(gè)定義在 runtime.h
中,Sched 定義在 proc.c
中。
- Sched 結(jié)構(gòu)就是調(diào)度器,它維護(hù)有存儲(chǔ) M 和 G 的隊(duì)列以及調(diào)度器的一些狀態(tài)信息等
- M 結(jié)構(gòu)是 Machine,系統(tǒng)線程,它由操作系統(tǒng)管理和調(diào)度的,goroutine 就是跑在 M 之上的; M 是一個(gè)很大的結(jié)構(gòu),里面維護(hù)小對(duì)象內(nèi)存 cache(mcache)、當(dāng)前執(zhí)行的 goroutine、隨機(jī)數(shù)發(fā)生器等等非常多的信息。
- P 結(jié)構(gòu)是 Processor,處理器,它的主要用途就是用來執(zhí)行 goroutine 的,它維護(hù)了一個(gè) goroutine 隊(duì)列,即 runqueue。 Processor 是讓我們從 N:1 調(diào)度到 M:N 調(diào)度的重要部分。
- G 是 goroutine 實(shí)現(xiàn)的核心結(jié)構(gòu),它包含了棧,指令指針,以及其他對(duì)調(diào)度 goroutine 很重要的信息,例如其阻塞的 channel。
我們分別用三角形,矩形和圓形表示 Machine Processor 和 Goroutine:
在單核處理器的場(chǎng)景下,所有 goroutine 運(yùn)行在同一個(gè) M 系統(tǒng)線程中,每一個(gè) M 系統(tǒng)線程維護(hù)一個(gè) Processor,任何時(shí)刻,一個(gè) Processor 中只有一個(gè) goroutine,其他 goroutine 在 runqueue 中等待。
一個(gè) goroutine 運(yùn)行完自己的時(shí)間片后,讓出上下文,回到 runqueue 中。 多核處理器的場(chǎng)景下,為了運(yùn)行 goroutines,每個(gè) M 系統(tǒng)線程會(huì)持有一個(gè) Processor 。
可以看到 Go 的并發(fā)用起來非常簡(jiǎn)單,用了一個(gè)語法糖將內(nèi)部復(fù)雜的實(shí)現(xiàn)結(jié)結(jié)實(shí)實(shí)的包裝了起來。
其內(nèi)部可以用下面這張圖來概述:
在單核處理器的場(chǎng)景下,所有 goroutine 運(yùn)行在同一個(gè) M 系統(tǒng)線程中,每一個(gè) M 系統(tǒng)線程維護(hù)一個(gè) Processor,任何時(shí)刻,一個(gè) Processor 中只有一個(gè) goroutine,其他 goroutine 在 runqueue 中等待。一個(gè) goroutine 運(yùn)行完自己的時(shí)間片后,讓出上下文,回到 runqueue 中。 多核處理器的場(chǎng)景下,為了運(yùn)行 goroutines,每個(gè) M 系統(tǒng)線程會(huì)持有一個(gè) Processor 。
在正常情況下,scheduler 會(huì)按照上面的流程進(jìn)行調(diào)度,但是線程會(huì)發(fā)生阻塞等情況,看一下goroutine對(duì)線程阻塞等的處理。
到此這篇關(guān)于Go 并發(fā)編程協(xié)程及調(diào)度機(jī)制詳情的文章就介紹到這了,更多相關(guān)Go 調(diào)度機(jī)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- golang并發(fā)編程中Goroutine 協(xié)程的實(shí)現(xiàn)
- 并發(fā)安全本地化存儲(chǔ)go-cache讀寫鎖實(shí)現(xiàn)多協(xié)程并發(fā)訪問
- 詳解Go如何實(shí)現(xiàn)協(xié)程并發(fā)執(zhí)行
- go語言限制協(xié)程并發(fā)數(shù)的方案詳情
- Go并發(fā):使用sync.WaitGroup實(shí)現(xiàn)協(xié)程同步方式
- 詳解Go多協(xié)程并發(fā)環(huán)境下的錯(cuò)誤處理
- Go 并發(fā)實(shí)現(xiàn)協(xié)程同步的多種解決方法
- Go 控制協(xié)程(goroutine)的并發(fā)數(shù)量
相關(guān)文章
golang1.23版本之前 Timer Reset方法無法正確使用
在Go 1.23之前,使用`time.Reset`函數(shù)時(shí)需要先調(diào)用`Stop`并明確從timer的channel中抽取出東西,以避免潛在的問題,然而,這在實(shí)際代碼中難以實(shí)現(xiàn),因?yàn)樵O(shè)置定時(shí)器狀態(tài)和發(fā)送channel的操作并不是原子的,在某些情況下,這會(huì)導(dǎo)致timer在不應(yīng)該觸發(fā)時(shí)提前觸發(fā)2025-01-01在golang xorm中使用postgresql的json,array類型的操作
這篇文章主要介紹了在golang xorm中使用postgresql的json,array類型的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-04-04使用gin框架搭建簡(jiǎn)易服務(wù)的實(shí)現(xiàn)方法
go語言web框架挺多的,本文就介紹了一下如何使用gin框架搭建簡(jiǎn)易服務(wù)的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12Go中使用操作符進(jìn)行數(shù)學(xué)運(yùn)算的示例代碼
在編程中有效地執(zhí)行數(shù)學(xué)運(yùn)算是一項(xiàng)需要開發(fā)的重要技能,本文主要介紹了Go中使用操作符進(jìn)行數(shù)學(xué)運(yùn)算的示例代碼,具有一定的參考價(jià)值,感興趣的可以了解一下2023-10-10Golang迭代如何在Go中循環(huán)數(shù)據(jù)結(jié)構(gòu)使用詳解
這篇文章主要為大家介紹了Golang迭代之如何在Go中循環(huán)數(shù)據(jù)結(jié)構(gòu)使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10詳解Go中如何進(jìn)行進(jìn)行內(nèi)存優(yōu)化和垃圾收集器管理
這篇文章主要為大家詳細(xì)介紹了Go中如何進(jìn)行進(jìn)行內(nèi)存優(yōu)化和垃圾收集器管理,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的小伙伴可以了解下2023-11-11Go語言字典(map)用法實(shí)例分析【創(chuàng)建,填充,遍歷,查找,修改,刪除】
這篇文章主要介紹了Go語言字典(map)用法,結(jié)合實(shí)例形式較為詳細(xì)的分析了Go語言字典的創(chuàng)建、填充、遍歷、查找、修改、刪除等操作相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-02-02