這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
《Go語言實戰》讀書筆記,未完待續,歡迎掃碼關注公眾號flysnow_org
,第一時間看後續筆記。覺得有協助的話,順手分享到朋友圈吧,感謝支援。
在談goroutine之前,我們先談談並發和並行。
一般的程式,如果沒有特別的要求的話,是順序執行的,這樣的程式也容易編寫維護。但是隨著科技的發展、業務的演化,我們不得不變寫可以並行的程式,因為這樣有很多好處。
比如你在看文章的時候,還可以聽著音樂,這就是系統的並行,同時可以做多件事情,充分的利用電腦的多核,提升的軟體啟動並執行效能。
在作業系統中,有兩個重要的概念:一個是進程、一個是線程。當我們運行一個程式的時候,比如你的IDE或者QQ等,作業系統會為這個程式建立一個進程,這個進程包含了運行這個程式所需的各種資源,可以說它是一個容器,是屬於這個程式的工作空間,比如它裡面有記憶體空間、檔案控制代碼、裝置和線程等等。
那麼線程是什麼呢?線程是一個執行的空間,比如要下載一個檔案,訪問一次網路等等。線程會被作業系統調用,來在不同的處理器上運行編寫的代碼任務,這個處理器不一定是該程式進程所在的處理。作業系統過的調度是作業系統負責的,不同的作業系統可能會不一樣,但是對於我們程式編寫者來說,不用關心,因為對我們都是透明的。
一個進程在啟動的時候,會建立一個主線程,這個主線程結束的時候,程式進程也就終止了,所以一個進程至少有一個線程,這也是我們在main
函數裡,使用goroutine的時候,要讓主線程等待的原因,因為主線程結束了,程式就終止了,那麼就有可能會看不到goroutine的輸出。
go語言中並髮指的是讓某個函數獨立於其他函數啟動並執行能力,一個goroutine就是一個獨立的工作單元,Go的runtime(運行時)會在邏輯處理器上調度這些goroutine來運行,一個邏輯處理器綁定一個作業系統線程,所以說goroutine不是線程,它是一個協程,也是這個原因,它是由Go語言運行時本身的演算法實現的。
這裡我們總結下幾個概念:
概念 |
說明 |
進程 |
一個程式對應一個獨立程式空間 |
線程 |
一個執行空間,一個進程可以有多個線程 |
邏輯處理器 |
執行建立的goroutine,綁定一個線程 |
調度器 |
Go運行時中的,分配goroutine給不同的邏輯處理器 |
全域運行隊列 |
所有剛建立的goroutine都會放到這裡 |
本地運行隊列 |
邏輯處理器的goroutine隊列 |
當我們建立一個goroutine的後,會先存放在全域運行隊列
中,等待Go運行時的調度器
進行調度,把他們分配給其中的一個邏輯處理器
,並放到這個邏輯處理器對應的本地運行隊列
中,最終等著被邏輯處理器
執行即可。
這一套管理、調度、執行goroutine的方式稱之為Go的並發。並發可以同時做很多事情,比如有個goroutine執行了一半,就被暫停執行其他goroutine去了,這是Go控制管理的。所以並發的概念和並行不一樣,並行指的是在不同的物理處理器上同時執行不同的程式碼片段,並行可以同時做很多事情,而並發是同時管理很多事情,因為作業系統和硬體的總資源比較少,所以並發的效果要比並行好的多,使用較少的資源做更多的事情,也是Go語言提倡的。
Go的並發原理我們剛剛講了,那麼Go的並行是怎樣的呢?其實答案非常簡單,多建立一個邏輯處理器
就好了,這樣調度器就可以同時分配全域運行隊列
中的goroutine到不同的邏輯處理器
上並存執行。
1234567891011121314151617 |
func main() {var wg sync.WaitGroupwg.Add(2)go func(){defer wg.Done()for i:=1;i<100;i++ {fmt.Println("A:",i)}}()go func(){defer wg.Done()for i:=1;i<100;i++ {fmt.Println("B:",i)}}()wg.Wait()} |
這是一個簡單的並發程式。建立一個goroutine是通過go
關鍵字的,其後跟一個函數或者方法即可。
這裡的sync.WaitGroup
其實是一個計數的訊號量,使用它的目的是要main
函數等待兩個goroutine執行完成後再結束,不然這兩個goroutine
還在啟動並執行時候,程式就結束了,看不到想要的結果。
sync.WaitGroup
的使用也非常簡單,先是使用Add
方法設設定計算機為2,每一個goroutine的函數執行完之後,就調用Done
方法減1。Wait
方法的意思是如果計數器大於0,就會阻塞,所以main
函數會一直等待2個goroutine完成後,再結束。
我們運行這個程式,會發現A和B首碼會交叉出現,並且每次啟動並執行結果可能不一樣,這就是Go調度器調度的結果。
預設情況下,Go預設是給每個可用的物理處理器都分配一個邏輯處理器,因為我的電腦是4核的,所以上面的例子預設建立了4個邏輯處理器,所以這個例子中同時也有並行的調度,如果我們強制只使用一個邏輯處理器,我們再看看結果。
123456789101112131415161718 |
func main() {runtime.GOMAXPROCS(1)var wg sync.WaitGroupwg.Add(2)go func(){defer wg.Done()for i:=1;i<100;i++ {fmt.Println("A:",i)}}()go func(){defer wg.Done()for i:=1;i<100;i++ {fmt.Println("B:",i)}}()wg.Wait()} |
設定邏輯處理器個數也非常簡單,在程式開頭使用runtime.GOMAXPROCS(1)
即可,這裡設定的數量是1。我們這時候再運行,會發現先列印A,再列印B。
這裡我們不要誤認為是順序執行,這裡之所以順序輸出的原因,是因為我們的goroutine執行時間太短暫了,還沒來得及切換到第2個goroutine,第1個goroutine就完成了。這裡我們可以把每個goroutine的執行時間拉長一些,就可以看到並發的效果了,這裡不再樣本了,大家自己試試。
對於邏輯處理器的個數,不是越多越好,要根據電腦的實際物理核心數,如果不是多核的,設定再多的邏輯處理器個數也沒用,如果需要設定的話,一般我們採用如下代碼設定。
1 |
runtime.GOMAXPROCS(runtime.NumCPU()) |
所以對於並發來說,就是Go語言本身自己實現的調度,對於並行來說,是和啟動並執行電腦的物理處理器的核心數有關的,多核就可以並行並發,單核只能並發了。
《Go語言實戰》讀書筆記,未完待續,歡迎掃碼關注公眾號flysnow_org
,第一時間看後續筆記。覺得有協助的話,順手分享到朋友圈吧,感謝支援。