1. 程式人生 > 實用技巧 >goroutine是如何啟停的

goroutine是如何啟停的

寫在前面

本文基於GoLang 1.14

在Go中,goroutine不過是一個Go結構,其中包含了關於正在執行的程式的資訊,如堆疊、程式計數器或其當前的作業系統執行緒。Go排程器會處理這些資訊,給它們提供執行時間。排程器在goroutine的啟動和退出時也要注意,這兩個階段需要小心管理。

啟動

建立一個goroutine非常的簡單,如下:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(1)

	go func() {
		fmt.Println("Hello")
		wg.Done()
	}()

	fmt.Println("Done")
	wg.Wait()
}

主函式在列印訊息之前,會啟動一個goroutine。由於goroutine會有自己的執行時間,Go會通知執行時建立一個新的goroutine,也就是說:

  • 建立堆疊。
  • 收集當前程式計數器或呼叫者的資料資訊。
  • 更新goroutine的內部資料,如ID或狀態。

但是,goroutine不會立即獲得任何執行時間。新建立的goroutine將被放到在本地佇列的開頭,並在下一輪GoLang排程器中執行。

把goroutine放在佇列的頭,目的是使它在當前goroutine之後第一個執行。如果有work-stealing的情況發生,它將在同一執行緒或另一執行緒上執行。

我們可以從下面的彙編程式碼中看到goroutine的建立:

一旦goroutine被建立並推送到本地的goroutine佇列中,它就直接進入主函式的下一條指令。

退出

當一個goroutine結束時,為了不浪費CPU時間,Go必須排程另一個goroutine。同時,它還會保留當前這個goroutine,以便以後重複使用。

然而,GoLang需要一種方法來感知goroutine的結束。這個控制是在建立goroutine的過程中。

在建立goroutine時,Go在將程式計數器設定為goroutine呼叫的真正函式之前,會將棧設定為一個名為goexit的函式,這樣可以保證goroutine在結束工作後呼叫函式goexit

關於上面的描述,我們通過以下程式碼進行展示一下:

package main

import (
	"fmt"
	"runtime"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(1)

	go func() {
		var skInt int
		for {
			_, file, line, ok := runtime.Caller(skInt)

			if !ok {
				break
			}

			fmt.Printf("%s:%d\n", file, line)
			skInt++
		}

		wg.Done()
	}()

	fmt.Println("Done")
	wg.Wait()
}

執行程式碼得到如下輸出:

F:\hello>go run main.go
Done
F:/hello/main.go:16
D:/Go/src/runtime/asm_amd64.s:1374

asm_amd64.s這個檔案中,我們可以看到有如下的函式的定義:

// The top-most function running on a goroutine
// returns to goexit+PCQuantum.
TEXT runtime·goexit(SB),NOSPLIT,$0-0
	BYTE	$0x90	// NOP
	CALL	runtime·goexit1(SB)	// does not return
	// traceback from goexit1 must hit code range of goexit
	BYTE	$0x90	// NOP

我們可以看到,GoLang將會切換到g0這個goroutine從而去排程其它的goroutine。

在我們程式碼中,我們也可以呼叫runtime.Goexit()去主動退出。

package main

import (
	"fmt"
	"runtime"
	"sync"
)

func main() {
	var wg sync.WaitGroup

	wg.Add(1)

	go func() {
		defer wg.Done()
		runtime.Goexit()

		fmt.Println("exist")
	}()

	wg.Wait()
}

這個函式將首先執行defer函式,然後當一個goroutine退出時,將呼叫之前看到的同一個函式。