如果說Go lang是靜態語言中的皇冠,那麼,Goroutine就是並行程式設計方式中的鑽石。Goroutine是Go語言設計體系中最核心的精華,它非常輕量,一個 Goroutine 只佔幾 KB,並且這幾 KB 就足夠 Goroutine 執行完,這就能在有限的記憶體空間內支援大量 Goroutine協程任務,方寸之間,運籌帷幄,用極少的成本獲取最高的效率,支援了更多的並行,毫無疑問,Goroutine是比Python的協程原理事件迴圈更高階的並行非同步程式設計方式。
為什麼Goroutine比Python的事件迴圈高階?是因為Go lang的排程模型GMP可以參與系統核心執行緒中的排程,這裡G為Goroutine,是被排程的最小單元;M是系統起了多少個執行緒;P為Processor,也就是CPU處理器,排程器的核心處理器,通常表示執行上下文,用於匹配 M 和 G 。P 的數量不能超過 GOMAXPROCS 設定數量,這個引數的預設值為當前電腦的總核心數,通常一個 P 可以與多個 M 對應,但同一時刻,這個 P 只能和其中一個 M 發生繫結關係;M 被建立之後需要自行在 P 的 free list 中找到 P 進行繫結,沒有繫結 P 的 M,會進入阻塞狀態,每一個P最多關聯256個G。
說白了,就是GMP和Python一樣,也是維護一個任務佇列,只不過這個任務佇列是通過Goroutine來排程,怎麼排程?通過Goroutine和系統執行緒M的協商,尋找非阻塞的通道,進入P的本地小佇列,然後交給系統內的CPU執行,藉此,充分利用了CPU的多核資源。
而Python的協程方式僅僅停留在使用者態,它沒法參與到執行緒核心的排程,彌補方式是單執行緒多協程任務下開多程序,Go lang則是全權交給Goroutine,使用者不需要參與底層操作,同時又可以利用CPU的多核資源。
首先預設情況下,golang程式還是由上自下的序列方式:
package main
import (
"fmt"
)
func job() {
fmt.Println("任務執行")
}
func main() {
job()
fmt.Println("任務執行完了")
}
程式返回:
任務執行
任務執行完了
這裡job中的列印函數是先於main中的列印函數。
現在,在執行job函數前面加上關鍵字go,也就是啟動一個goroutine去執行job這個函數:
package main
import (
"fmt"
"time"
)
func job() {
fmt.Println("任務執行")
}
func main() {
go job()
fmt.Println("任務執行完了")
time.Sleep(time.Second)
}
注意,開啟Goroutine是在函數執行的時候開啟,並非宣告的時候,程式返回:
任務執行完了
任務執行
可以看到,執行順序顛倒了過來,首先為什麼會先列印任務執行完了,是因為系統在建立新的Goroutine的時候需要耗費一些資源,因為就算只有幾kb,也需要時間來建立,而此時main函數所在的goroutine是繼續執行的。
第二,為什麼要人為的把main函數延遲一秒鐘?
因為當main()函數返回的時候main所在的Goroutine就結束了,所有在main()函數中啟動的goroutine會一同結束,所以這裡必須人為的「阻塞」一下main函數,讓它後於job結束,有點像公園如果要關門必須等最後一個遊客走了才能關,否則就把遊客關在公園裡了,出不去了。
與此同時,此邏輯和Python中的執行緒阻塞邏輯非常一致,用過Python多執行緒的朋友肯定知道要想讓所有子執行緒都執行完畢,必須阻塞主執行緒,不能讓主執行緒提前執行完,這和Goroutine有異曲同工之妙。
在Go lang中實現並行程式設計就是如此輕鬆,我們還可以啟動多個Goroutine:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func job(i int) {
defer wg.Done() // 協程結束就通知
fmt.Println("協程任務執行", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 啟動協程任務後入隊
go job(i)
}
wg.Wait() // 等待所有登記的goroutine都結束
fmt.Println("所有任務執行完畢")
}
程式返回:
協程任務執行 8
協程任務執行 9
協程任務執行 5
協程任務執行 0
協程任務執行 1
協程任務執行 4
協程任務執行 7
協程任務執行 2
協程任務執行 3
協程任務執行 6
所有任務執行完畢
這裡我們摒棄了相對土鱉的time.Sleep(time.Second)方式,而是採用sync包的WaitGroup方式,原理是當啟動協程任務後,在WaitGroup登記,當每個協程任務執行完成後,通知WaitGroup,直到所有的協程任務都執行完畢,然後再執行main函數所在的協程,所以「所有任務執行完畢」會在所有協程任務執行完畢後再列印。
我們再來看看,如果是Python,會怎麼做?
import asyncio
import random
async def job(i):
print("協程任務執行{}".format(i))
await asyncio.sleep(random.randint(1,5))
print("協程任務結束{}".format(i))
async def main():
tasks = [asyncio.create_task(job(i)) for i in range(10)]
res = await asyncio.gather(*tasks)
if __name__ == '__main__':
asyncio.run(main())
程式返回:
協程任務執行0
協程任務執行1
協程任務執行2
協程任務執行3
協程任務執行4
協程任務執行5
協程任務執行6
協程任務執行7
協程任務執行8
協程任務執行9
協程任務結束0
協程任務結束1
協程任務結束3
協程任務結束6
協程任務結束9
協程任務結束8
協程任務結束2
協程任務結束4
協程任務結束5
協程任務結束7
可以看到,Python協程工作的前提是,必須在同一個事件迴圈中,同時邏輯內必須由使用者來手動切換,才能達到「並行」的工作方式,假設,如果我們不手動切換呢?
import asyncio
import random
async def job(i):
print("協程任務執行{}".format(i))
print("協程任務結束{}".format(i))
async def main():
tasks = [asyncio.create_task(job(i)) for i in range(10)]
res = await asyncio.gather(*tasks)
if __name__ == '__main__':
asyncio.run(main())
程式返回:
協程任務執行0
協程任務結束0
協程任務執行1
協程任務結束1
協程任務執行2
協程任務結束2
協程任務執行3
協程任務結束3
協程任務執行4
協程任務結束4
協程任務執行5
協程任務結束5
協程任務執行6
協程任務結束6
協程任務執行7
協程任務結束7
協程任務執行8
協程任務結束8
協程任務執行9
協程任務結束9
一望而知,只要你不手動切任務,它就立刻回到了「序列」的工作方式,同步的執行任務,那麼協程的意義在哪兒呢?
所以,歸根結底,Goroutine除了可以極大的利用系統多核資源,它還能幫助開發者來切換協程任務,簡化開發者的工作,說白了就是,不懂協程工作原理,也能照貓畫虎寫go lang程式碼,但如果不懂協程工作原理的前提下,寫Python協程並行邏輯呢?恐怕夠嗆吧。
綜上,Goroutine的工作方式,就是多個協程在多個執行緒上切換,既可以用到多核,又可以減少切換開銷。但有光就有影,有利就有弊,Goroutine確實不需要開發者過度參與,但這樣開發者就少了很多自由度,一些客製化化場景下,就只能採用單一的Goroutine手段,比如一些純IO密集型任務場景,像爬蟲,你有多少cpu的意義並不大,因為cpu老是等著你的io操作,所以Python這種協程工作方式在純IO密集型任務場景下並不遜色於Goroutine。