Go语言36讲笔记--17 go语句及其执行规则(下)

106 阅读2分钟

问题 1:怎样才能让主 goroutine 等待其他 goroutine?

  1. sleep,最简单粗暴。
  2. 使用通道类型。
  3. sync.WaitGroup类型。

问题 2:怎样让我们启用的多个 goroutine 按照既定的顺序运行?

问题背景:

go函数的实际执行顺序往往与其所属的go语句的执行顺序(或者说 goroutine 的启用顺序)不同,而且默认情况下的执行顺序是不可预知的。 在问题1的代码上做进一步的改写

demo1

package main

import (
	"fmt"
	//"time"
)

func main() {
	num := 10
	sign := make(chan struct{}, num)
	//类型字面量struct{}有些类似于空接口类型interface{},
	//它代表了既不包含任何字段也不拥有任何方法的空结构体类型。

	for i := 0; i < num; i++ {
		go func() {
			fmt.Println(i)
			sign <- struct{}{}
		}()
	}

	// 办法1。
	//time.Sleep(time.Millisecond * 500)

	// 办法2。
	for j := 0; j < num; j++ {
		<-sign
	}
}

---
9
10
3
10
10
10
10
10
10
10
  1. 保证每个 goroutine 都可以拿到一个唯一的整数。
for i := 0; i < 10; i++ {
        //在go语句被执行时,我们传给go函数的参数i会先被求值
        //因此得到了当次迭代的序号
	go func(i int) {
		fmt.Println(i)
	}(i)
}
for i := uint32(0); i < 10; i++ {

	go func(i uint32) {
        
		fn := func() {//声明了一个匿名的函数,并把它赋给了变量fn
                
			fmt.Println(i)
                        
		}//函数功能:打印go函数的参数i的值
                
		trigger(i, fn) //参数:uint32类型的参数i, func()类型的参数fn
                
                //func()代表的是既无参数声明也无结果声明的函数类型。
	}(i)
        
}
trigger := func(i uint32, fn func()) {
	for {
		if n := atomic.LoadUint32(&count); n == i {
			fn()
			atomic.AddUint32(&count, 1)
			break
		}
		time.Sleep(time.Nanosecond)
	}
}

trigger函数会不断地获取一个名叫count的变量的值,并判断该值是否与参数i的值相同。

如果相同,那么就立即调用fn代表的函数,然后把count变量的值加1,最后显式地退出当前的循环。

否则,我们就先让当前的 goroutine“睡眠”一个纳秒再进入下一个迭代。

🐖Ⅰ:操作变量count的时候使用的都是原子操作。

这是由于trigger函数会被多个 goroutine 并发地调用,所以它用到的非本地变量count,就被多个用户级线程共用了。因此,对它的操作就产生了竞态条件(race condition),破坏了程序的并发安全性

所以,我们总是应该对这样的操作加以保护,在sync/atomic包中声明了很多用于原子操作的函数

由于选用的原子操作函数对被操作的数值的类型有约束,所以对count以及相关的变量和参数的类型进行了统一的变更(由int变为了uint32)。 宏观理解 纵观count变量、trigger函数以及改造后的for语句和go函数,我要做的是,让count变量成为一个信号,即它的值总是下一个可以调用打印函数的go函数的序号

这个序号其实就是启用 goroutine 时,那个当次迭代的序号。

从而保证go函数实际的执行顺序与go语句的执行顺序完全一致。

此外,这里的trigger函数实现了一种自旋(spinning)。除非发现条件已满足,否则它会不断地进行检查。

demo2

package main

import (
	"fmt"
	"sync/atomic"
	"time"
)


func main() {
	var count uint32
        
	trigger := func(i uint32, fn func()) {
		for {
			if n := atomic.LoadUint32(&count); n == i {
				fn()
				atomic.AddUint32(&count, 1)
				break
			}
			time.Sleep(time.Nanosecond)
		}
	}
        
	for i := uint32(0); i < 10; i++ {
		go func(i uint32) {
			fn := func() {
				fmt.Println(i)
			}
			trigger(i, fn)
		}(i)
	}
        
	trigger(10, func() {})
}

---
0
1
2
3
4
5
6
7
8
9

思考题

1.runtime包中提供了哪些与模型三要素 G、P 和 M 相关的函数?(模型三要素内容在上一篇)

pkg.go.dev/runtime