为什么两个 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
所以我有两个问题:
- 为什么f1的所有Printl()都严格执行完f2的Println()之后执行(我以为他们一定是混在一起了)
- 为什么我在代码中更改 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。使用这些技术可以保证较大任务的精确排序。
然而,预测这些协调检查点之间 运行 的单个指令的执行顺序是不可能的。
我绝对是 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
所以我有两个问题:
- 为什么f1的所有Printl()都严格执行完f2的Println()之后执行(我以为他们一定是混在一起了)
- 为什么我在代码中更改 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。使用这些技术可以保证较大任务的精确排序。
然而,预测这些协调检查点之间 运行 的单个指令的执行顺序是不可能的。