【Go并发编程】Goroutine的基本使用

时间:2023-02-20 18:07:16

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。