go-go

1 概述

Go 語言中的協程是由 Go 運行時(runtime)調度器(scheduler)進行管理和調度的。

當程序啓動時,Go 運行時會默認啓動一個主協程。主協程會創建其他的子協程,這些協程會被分配到不同的系統線程上進行執行。當某個協程發生阻塞時,Go 運行時會將該協程掛起並讓出 CPU,轉而執行其他協程,以充分利用系統資源。

2 go關鍵字

在 Go 語言中,創建協程非常簡單,只需要在函數調用前加上 go 關鍵字即可。我們來試試看:

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("before go A at: " + time.Now().String())
	go A()
	fmt.Println("after go A at: " + time.Now().String())
	return
}

func A() {
	fmt.Println("inside go A at" + time.Now().String())
}

輸出

before go A at: 2024-03-22 20:06:54.811707 +0800 CST m=+0.000095334
after go A at: 2024-03-22 20:06:54.811881 +0800 CST m=+0.000270001

很奇怪,並沒有輸出 新起的那個協程的打印。

原因有兩點:

  • 發起協程並不是阻塞的,發起之後,當前協程繼續往下走,並不會等待新起協程所調用的方法執行完;
A() // 調用A函數,等待A函數執行完的返回結果
go A() // 發起協程執行A函數,不等待A函數執行,當前協程直接往下走
  • main方法所在的協程一直在執行,直到最後return也沒有讓出過,導致新發起的協程並沒有機會被執行。

2.1 協程的執行

而我們知道有很多方法可以觸發讓出,比如系統調用。

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("before go A at: " + time.Now().String())
	go A()
	time.Sleep(1 * time.Second)
	fmt.Println("after go A at: " + time.Now().String())
	return
}

func A() {
	fmt.Println("inside go A at: " + time.Now().String())
}

輸出

before go A at: 2024-03-22 20:08:13.069353 +0800 CST m=+0.000218709
inside go A at: 2024-03-22 20:08:13.069625 +0800 CST m=+0.000491626
after go A at: 2024-03-22 20:08:14.071853 +0800 CST m=+1.002744001

我們在 go A() 後面加了一個 time.Sleep(),觸發了系統調用,主函數的協程讓出,從而執行了協程。

2.2 用匿名函數發起協程

當然,我們這裏 go關鍵字後面調用的是一個 命名函數A,其實也可以直接調用 匿名函數。

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("before go func at: " + time.Now().String())
	go func() {
		fmt.Println("inside go func at: " + time.Now().String())
	}()
	time.Sleep(1 * time.Second)
	fmt.Println("after go func at: " + time.Now().String())
	return
}

輸出

before go func at: 2024-03-22 20:13:05.183352 +0800 CST m=+0.000230251
inside go func at: 2024-03-22 20:13:05.183605 +0800 CST m=+0.000482959
after go func at: 2024-03-22 20:13:06.188708 +0800 CST m=+1.005603459

效果是一樣的。

2.3 多個協程的執行

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("before go func at: " + time.Now().String())
	go func() {
		fmt.Println("inside go func1 at: " + time.Now().String())
	}()
	go func() {
		fmt.Println("inside go func2 at: " + time.Now().String())
	}()
    go func() {
		fmt.Println("inside go func3 at: " + time.Now().String())
	}()
	time.Sleep(1 * time.Second)
	fmt.Println("after go func at: " + time.Now().String())
	return
}

輸出

before go func at: 2024-03-22 20:32:20.556914 +0800 CST m=+0.000219418
inside go func3 at: 2024-03-22 20:32:20.557247 +0800 CST m=+0.000552293
inside go func1 at: 2024-03-22 20:32:20.557248 +0800 CST m=+0.000554251
inside go func2 at: 2024-03-22 20:32:20.557253 +0800 CST m=+0.000558793
after go func at: 2024-03-22 20:32:21.560869 +0800 CST m=+1.004190918

這裏我們看到,連續發起了3個協程,當前協程讓出後,這3個協程的執行順序是不固定的。這是因爲go的調度器只是儘可能地讓每個協程都有均等的機會被調度到並被執行,而並不能保證它們的執行順序。

2.4 多次讓出

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("before go func at: " + time.Now().String())
	go func() {
		fmt.Println("inside go func1 before sleep at: " + time.Now().String())
		time.Sleep(10 * time.Millisecond)
		fmt.Println("inside go func1 after sleep at: " + time.Now().String())
	}()
	go func() {
		fmt.Println("inside go func2 before sleep at: " + time.Now().String())
		time.Sleep(20 * time.Millisecond)
		fmt.Println("inside go func2 after sleep at: " + time.Now().String())
	}()
	go func() {
		fmt.Println("inside go func3 before sleep at: " + time.Now().String())
		time.Sleep(30 * time.Millisecond)
		fmt.Println("inside go func3 after sleep at: " + time.Now().String())
	}()
	time.Sleep(20 * time.Millisecond)
	fmt.Println("after go func at: " + time.Now().String())
	return
}

輸出

before go func at: 2024-03-22 20:41:51.424324 +0800 CST m=+0.000125417
inside go func1 before sleep at: 2024-03-22 20:41:51.424463 +0800 CST m=+0.000264251
inside go func3 before sleep at: 2024-03-22 20:41:51.424489 +0800 CST m=+0.000289917
inside go func2 before sleep at: 2024-03-22 20:41:51.424481 +0800 CST m=+0.000282792
inside go func1 after sleep at: 2024-03-22 20:41:51.436992 +0800 CST m=+0.012793751
inside go func2 after sleep at: 2024-03-22 20:41:51.446344 +0800 CST m=+0.022145626
after go func at: 2024-03-22 20:41:51.446349 +0800 CST m=+0.022150876

可以看到:

  • 每次協程遇到讓出的時候,都會暫停執行,讓出給其他協程執行;
  • 當其他協程執行完之後,也並不是當然地回到當前協程繼續執行,有可能會調度到第三個協程;
  • func3的after語句沒有被打印出來,原因是當func2 after執行完後,CPU調度到了main函數所在的協程;而main函數協程執行完了之後,程序就退出了,其他的協程都被直接打斷了;

至此,我們簡單地瞭解了go的協程的一些運行情況。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章