【Go並行程式設計】Goroutine的基本使用

2023-02-20 18:00:24

goroutine是什麼

goroutine即協程,使用go關鍵字開啟一個協程非同步執行程式碼。

注意,main函數也是個goroutine。

基本使用

使用go執行子任務,會交替執行(和時間片一樣)。

主goroutine退出後,其它的工作goroutine也會自動退出(有點父子程序的感覺):

package main

import (
    "fmt"
    "time"
)

func newTask() {
    i := 0
    for {
        i++
        fmt.Printf("new goroutine: i = %d\n", i)
        time.Sleep(1 * time.Second) //延時1s
    }
}

func main() {
    //建立一個 goroutine,啟動另外一個任務
    go newTask()

    i := 0
    //main goroutine 迴圈列印
    for {
        i++
        fmt.Printf("main goroutine: i = %d\n", i)
        time.Sleep(1 * time.Second) //延時1s
    }
    //這裡是加入了死迴圈,如果去掉,則程式會直接退出。
}

多個協程的順序是不一定的。

var _ = runtime.GOMAXPROCS(3)
var a, b int
func u1() {
    a = 1
    b = 2
}
func u2() {
    a = 3
    b = 4
}
func p() {
    println(a)
    println(b)
}
func main() {
    go u1()    // 多個 goroutine 的執行順序不定
    go u2()    
    go p()
    time.Sleep(1 * time.Second)
}

runtime包

Gosched

runtime.Gosched() //讓別人先執行,需要同時需要時間片的時候才會有效,對方如果已經停了就還是自己執行。

就像孔融讓梨(梨就是CPU時間片),A遇到runtime.Gosched()就先給B吃(讓出時間片),但是如果B已經吃完了(B已經不需要時間片了),A就開始吃(A則開始佔用CPU)。

func main() {
    //建立一個goroutine
    go func(s string) {
        for i := 0; i < 2; i++ {
            fmt.Println(s)
        }
    }("world")
 
    for i := 0; i < 2; i++ {
        runtime.Gosched() //import "runtime"
        /*
            遮蔽runtime.Gosched()執行結果如下:
                hello
                hello
 
            沒有runtime.Gosched()執行結果如下:
                world
                world
                hello
                hello
        */
        fmt.Println("hello")
    }
}

優先排程:

你的程式可能出現一個 goroutine 在執行時阻止了其他 goroutine 的執行,比如程式中有一個不讓排程器執行的 for 迴圈:

排程器會在 GC、Go 宣告、阻塞 channel、阻塞系統呼叫和鎖操作後再執行,也會在非行內函式呼叫時執行:

func main() {
    done := false
    go func() {
        done = true
    }()
		//這裡佔用了排程,協程無法啟動
    for !done {
			println("not done !")    // 並不內聯執行
    }
    println("done !")
}

//可以新增 -m 引數來分析 for 程式碼塊中呼叫的行內函式

修改:

func main() {
    done := false
    go func() {
        done = true
    }()
    for !done {
				runtime.Gosched() 
    }
    println("done !")
}

Goexit

runtime.Goexit() //將立即終止當前 goroutine 執⾏,排程器確保所有已註冊 defer延遲呼叫被執行。

package main

import (
	"fmt"
	"runtime"
	"time"
)

//呼叫 runtime.Goexit() 將立即終止當前 goroutine 執⾏,排程器確保所有已註冊 defer延遲呼叫被執行。

func main() {
	go func() {
		defer fmt.Println("A.defer")

		func() {
			defer fmt.Println("B.defer")
			runtime.Goexit() // 終止當前 goroutine, import "runtime"
			fmt.Println("B") // 不會執行
		}()

		defer fmt.Println("C.defer") //還沒來得及註冊,不會執行

		fmt.Println("A") // 不會執行
	}() //別忘了()

	//死迴圈,目的不讓主goroutine結束
	for {
		time.Sleep(1 * time.Second)
	}
}

//執行結果:
//B.defer
//A.defer

GOMAXPROCS

呼叫 runtime.GOMAXPROCS() 用來設定可以平行計算的CPU核數的最大值,並返回之前的值。

範例程式碼:

func main() {
    //n := runtime.GOMAXPROCS(1)
    //列印結果:111111111111111111110000000000000000000011111...
    n := runtime.GOMAXPROCS(2)
    //列印結果:010101010101010101011001100101011010010100110...
    fmt.Printf("n = %d\n", n)

    for {
        go fmt.Print(0)
        fmt.Print(1)
    }
}

在第一次執行(runtime.GOMAXPROCS(1))時,最多同時只能有一個goroutine被執行。所以會列印很多1。

過了一段時間後,GO排程器會將其置為休眠,並喚醒另一個goroutine,這時候就開始列印很多0了,在列印的時候,goroutine是被排程到作業系統執行緒上的。

在第二次執行(runtime.GOMAXPROCS(2))時,我們使用了兩個CPU,所以兩個goroutine可以一起被執行,以同樣的頻率交替列印0和1。