1. 程式人生 > >goroutine語句及其執行順序

goroutine語句及其執行順序

goroutine代表著併發程式設計模式中的使用者級執行緒。

 作業系統本身提供了程序和執行緒這兩種併發執行程式的工具。程序,描述的就是程式的執行過程,是執行著的程式的代表。執行緒,總是在程序之內的,可以被視為程序中執行著的控制流(或者說程式碼執行的流程)。

 一個程序至少會包含一個執行緒。如果一個程序只包含一個執行緒,那麼它裡面的所有程式碼都會被序列地執行。每個程序的第一個執行緒都會隨著該程序的啟動而被建立,它們可以被稱為其所屬程序的主程式。

 相對應的,如果一個程序中包含了多個執行緒,那麼其中的程式碼可以被併發地執行。除了程序的第一個執行緒之外,其他的執行緒都是由程序中已存在的執行緒創建出來的。

 也就是說,主執行緒之外的其他執行緒只能由程式碼顯式地建立和銷燬。這需要我們在編寫程式的時候進行手動控制,作業系統以及程序本身並不會幫我們下達這樣的指令,它們只會忠實地執行我們的指令。

 不過,在Go程式中,Go語言的執行時(runtime)系統會幫組我們自動地建立和銷燬系統級的執行緒。這裡的系統級執行緒指的就是我們剛剛說過的作業系統提供的執行緒。

 而對應的使用者級執行緒指的是架設在系統級執行緒之上的,由使用者(或者說我們編寫的程式)完全控制的程式碼執行流程。使用者級執行緒的建立、銷燬、狀態變更以及其中的程式碼和資料都完全需要我們的程式自己去實現和處理。

 這帶來了很多優勢,比如,因為它們的建立和銷燬並不用通過作業系統去做,所以速度會很快,又比如,由於不用等著作業系統去排程它們的執行,所以往往會很容易控制並且可以很靈活。

 但是,劣勢也是有的,最明顯也最重要的一個劣勢就是複雜。如果我們只使用了系統級執行緒,那麼我們只要指明需要新執行緒執行的程式碼片段,並且下達建立或銷燬執行緒的指令就好啦,其他的一切具體實現都會由作業系統代勞的。

 但是,如果使用使用者級執行緒,我們就不得不既是指令下達者,又是指令執行者。我們必須全權負責與使用者級執行緒有關的所有具體操作。

 作業系統不但不會幫忙,還會要求我們的具體實現必須與它正確地對接,否則使用者級執行緒就無法被併發地,甚至正確地執行。

 不過別擔心,Go語言不但有獨特的併發程式設計模型,以及使用者級執行緒goroutine,還擁有強大的用於排程goroutine、對接系統級執行緒的排程器。

這個排程器是Go語言執行時系統的重要組成部分,它主要負責統籌調配Go併發程式設計模型中的三個主要元素。即:G(goroutine 的縮寫)、P(processor的縮寫)和M(machine的縮寫)。

 其中的M指代的就是系統級執行緒。而P指的是一種可以承載若干個G,且能夠使這些G適時地與M進行對接,並得到真正執行的中介。

 從巨集觀上說,G和M由於P的存在可以呈現出多對多的關係。當一個正在與某個M對接並執行著的G,需要因某個事件(比如等待I/O或鎖的解除)而暫停執行的時候呀,排程器總會及時地發現,並把這個G與那個M分離開,以釋放計算資源提供那些等待執行的G使用。

 而當一個G需要恢復執行的時候,排程器又會盡快地為它尋找空閒的計算資源(包括M)並安排執行。另外,當M不夠用時,排程器會幫我們向作業系統申請新的系統級執行緒,而當某個M已無用時,排程器又會負責把它及時地銷燬掉。

 正因為排程器幫組我們做了很多事,所以我們的Go程式才總是能高效地利用作業系統和計算機資源。程式中的所有goroutine也都會被充分地排程,其中的程式碼也都會被併發地執行,即使這樣的goroutine有數以萬計,也仍然可以如此。

demo:

package main

import "fmt" 

func main() {
    for i:=0; i<10; i++ {
      go func() {
          fmt.Println(i)
      }()
    }
}

結果:不會有任何內容被列印。

原因:與一個程序總會有一個主執行緒類似,每一個獨立的Go程式在執行時也總會有一個主goroutine。這個主goroutine會在Go程式的執行準備工作完成後被自動地啟用,並不需要我們做任何手動的操作。

每條go語言一般都會攜帶一個函式呼叫,這個被呼叫的函式常常被稱為go函式,而主goroutine的go函式就是那個作為程式入口的main函式。

一定要注意,go函式真正被執行的時間總會與所屬的go語句被執行的時間不同。當程式執行到一條go語句時,Go語言的執行時系統,會試圖從某個存放空閒的G佇列中獲取一個G(也就是goroutine),它只有在找不到空閒G的情況下才會去建立一個新的G.

這也就是為什麼說“啟用”一個goroutine而不說“建立”一個goroutine的原因。已存在的goroutine總是會被有限複用。

然後,建立G的成本也是非常低的。建立一個G並不會像新建一個程序或者一個系統級執行緒那樣,必須通過作業系統的系統呼叫來完成,在Go語言的執行時系統內部就可以完全做到了,更何況一個G僅相當於需要併發執行程式碼片段服務的上下文環境而已。

在拿到了一個空閒的G之後,Go語言執行時系統會用這個G去包裝當前的那個go函式(或者說該函式中的那些程式碼),然後再把這個G追加到某個存放可執行的G的佇列中。

這類佇列中的G總是會按照先入先出的順序,很快地由執行時系統內部的排程器安排執行。雖然這會很快,但是由於上面所說的那些準備工作還是不可避免的,所以耗時還是存在的。

因此,go函式的執行時間總是會明顯滯後於它所屬的go語句的執行時間。

在demo中,一旦主goroutine中的程式碼執行完畢,當前的Go程式就會結束執行。還未執行的goroutine就會關閉。

但是,Go程式並不會去保證這些goroutine會以怎樣的順序執行。由於主goroutine會與我們手動啟用的其他goroutine一起排程,又因為排程器有可能會在goroutine的程式碼只執行了一部分的時候暫停,以期所有的goroutine有更公平的執行機會。

所以哪個goroutine先執行完,哪個goroutine後執行完往往是不可預知的,除非我們使用了某種Go語言提供的方式進行人為干預。

所以,demo的結果:絕大部分情況下丟失不會有任何內容打印出來,但不排除少部分情況有內容列印,如:10個9,亂序的0-9