为什么两个 goroutines 的控制台输出看起来是同步的

Why do console outputs of two goroutines looks like synchronous

我绝对是 Go 并发的新手。我试图用两个 gouroutines 产生竞争条件并编写了以下代码:

var x int = 2

func main() {

    go f1(&x)

    go f2(&x)

    time.Sleep(time.Second)
    fmt.Println("Final value of x:", x)

}

func f1(px *int) {
    for i := 0; i < 10; i++ {
        *px = *px * 2
        fmt.Println("f1:", *px)
    }
}

func f2(px *int) {
    for i := 0; i < 10; i++ {
        *px = *px + 1
        fmt.Println("f2:", *px)
    }
}

并且在每个输出变体中,控制台中都有 f2 的所有输出行,只有在那之后才有 f1 的输出。这是一个例子:

f2: 3
f2: 4
f2: 5
f2: 6
f2: 7
f2: 8
f2: 9
f2: 10
f2: 21
f2: 22
f1: 20
f1: 44
f1: 88
f1: 176
f1: 352
f1: 704
f1: 1408
f1: 2816
f1: 5632
f1: 11264
Final value of x: 11264

但是你可以看到 f1 的一些执行确实是在 f2 的执行之间进行的:

f2: 10
f2: 21

所以我有两个问题:

  1. 为什么f1的所有Printl()都严格执行完f2的Println()之后执行(我以为他们一定是混在一起了)
  2. 为什么我在代码中更改 goroutines 的顺序
    go f2(&x)
    go f1(&x)

而不是

    go f1(&x)
    go f2(&x)

输出行的顺序反之亦然,f1第一,f2'2第二。我的意思是 gouroutines 在代码中的顺序如何影响它们的执行?

首先,您看到的行为是由于 tight-loop。 Go 调度程序无法合理地知道如何分担工作负载,因为您的循环很短并且不会花费大量时间(例如低于 10 毫秒阈值)

Go 调度器的工作原理是一个非常复杂的话题,并且在 Go 版本中发生了变化,但引用 this artcile:

If loops don’t contain any preemption points (like function calls, or allocate memory), they will prevent other goroutines from running

抢占通常要到 10 毫秒后才会发生。

在现实世界中,处理循环通常会调用一些阻塞调用(数据库操作、REST/gRPC 调用等)——这会提示 Go 调度程序将其他 goroutine 设置为“可运行”。您可以通过在循环中插入 time.Sleep 来在代码中模拟此操作:https://play.golang.org/p/_C3QOUMNOaU

还有其他方法可以放弃 (runtime.Gosched),但通常应避免使用这些技巧。避免 tight-loops 并让日程安排完成。

执行顺序

当涉及多个 goroutines 时——正如@Marc 评论的那样——goroutines 之间没有协调,执行顺序是 non-deterministic。

Go 有很多工具可以用来协调 go-routine 活动:

阻塞当前 goroutine 并允许调度其他 goroutine。使用这些技术可以保证较大任务的精确排序。

然而,预测这些协调检查点之间 运行 的单个指令的执行顺序是不可能的。