Golang中WaitGroup的使用问题求助 - 附代码示例play.golang.org/p/HvHQbUTT6GJ

Golang中WaitGroup的使用问题求助 - 附代码示例play.golang.org/p/HvHQbUTT6GJ 在这段代码中,https://play.golang.org/p/HvHQbUTT6GJ,为什么 bar(用这个词是否正确?)会在 foo 之前执行?

10 回复

我想目前这样就挺好的。你在另一个话题中提到过标记问题为已解决?我该如何操作?如果标记了,以后还能回来查看学习吗?

更多关于Golang中WaitGroup的使用问题求助 - 附代码示例play.golang.org/p/HvHQbUTT6GJ的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


hollowaykeanho:

启动另一个进程

这个短语本身就很有意思,我在谷歌上搜索时得到了一些有趣的定义。

hollowaykeanho:

关于并发的问题

非常有趣。

这并不一定需要。

实际上在这种情况下,foo()bar() 的执行顺序是未定义的,理论上也可能交替执行。

(不过在 Playground 上总是会产生这样的输出,因为该环境的设置方式使得调度或一般的随机性具有可重现性)

bar 首先运行主要是因为 main() 进程仍在运行,并且在 foo() 委托(或实际术语:分叉)之后立即被指示执行 bar()。请记住 main() 本身仍然是一个进程。

当新进程被分叉时,与已经是运行进程的 main() 进程相比,它需要一些(编辑:不可预测的)时间来设置才能执行。

hollowaykeanho:

收到指示要求在 foo() 执行后立即执行 bar()

因此在我看来,输出结果中 foo 应该会先出现。

尝试使用引用按钮时,这个回复不知何故出现在我对 NobbZ 的回复之前。

cherilexvold1974:

标记问题为已解决?我该怎么做

你可以查看所有回复的帖子,找出最合适的答案。然后注意每个回复旁边都有一个复选框。勾选该复选框即可将该回复标记为你问题的解决方案。

11e8347fb7cf1b9d039b85a5d653b96cf1b47ea8

cherilexvold1974:

我以后还能回来查看吗?

你确实可以收藏自己的问题,可以通过浏览器收藏夹或论坛收藏功能。进入问题页面并展开选项(三个点),那里有一个收藏图标。点击它即可。

NobbZ:

也可能是交错执行的。

这让我感到困惑。这具体是什么意思?

通常可重现的随机性

你的意思是执行顺序是随机的吗?也就是说 foo 可能在 bar 之前运行,也可能 barfoo 之前运行?

hollowaykeanho:

@Noobz,启动顺序在代码中是有明确规定的,具体是第18行(委托)然后到第19行(执行 bar())。然而,在单核CPU中,mainfoo 这两个goroutine的并发执行顺序是不可预测的。

所以这听起来像是执行顺序是随机的。

或者这是否…

hollowaykeanho:

启动顺序在代码中是有明确规定的,具体是第18行(委托)然后到第19行(执行 bar()

意味着执行顺序并非随机?

难道

go foo()

是以某种方式告诉 foobar 之后运行吗?

cherilexvold1974:

那么这听起来执行是随机的。

或者这是否…

cherilexvold1974:

是否以某种方式

go foo()

告诉 foo 在 bar 之后运行?

go foo() 并不是说必须在 bar() 之后运行,而是启动另一个进程来运行 foo()。在运行实际的 foo() 之前,这个启动过程有它自己的元进程需要初始化。这就是为什么与已建立的进程 bar() 相比,foo() 会稍后出现。

另外,请记住,使用 goroutine 委托进程并不能保证它会立即启动。这就是为什么 foo() 看起来启动较晚,但我无法确定它需要多长时间才能开始执行 foo()

执行是随机的,因为无法确定在单个 CPU 上同时运行的是哪个进程(barfoo),所以输出是混乱的。请注意 Playground 的输出和我引用的输出之间的差异。这些输出并不一致。

至于原因,你可以参考这个帖子:关于并发的问题

bar 首先运行主要是因为 main() 进程仍在运行,并且在 foo() 委托(或实际术语:分叉)之后立即被指示执行 bar()。请记住,main() 本身仍然是一个进程。

当一个新的进程被分叉时,与已经是运行中的进程的 main() 进程相比,它需要一些(编辑:不可预测的)时间来设置然后执行。

事实上,在这种情况下,foo()bar() 的执行顺序是未定义的,理论上也可能是交错的。

很好的提出了那个重要的观点。谢谢! 😅

我在本地系统中运行了你的代码,它们始终一致:bar()foo() 之前。但是请注意,在并发中,它们永远不会像在 Playground 中那样完美结束。以下是我本地系统的一次运行结果(经过 20 次运行后):

OS               linux
ARCH             amd64
CPUs             8
Goroutines       1
bar: 0
bar: 1
bar: 2
bar: 3
bar: 4
bar: 5
bar: 6
bar: 7
bar: 8
bar: 9
CPUs             8
foo: 0
foo: 1
foo: 2
foo: 3
foo: 4
foo: 5
foo: 6
foo: 7
foo: 8
foo: 9
Goroutines       2

请记住,并发不是并行。那是完全不同的概念。


@Noobz,启动顺序在代码中是有定义的,具体是第 18 行(委托)然后到第 19 行(执行 bar())。然而,在单个 CPU 上,mainfoo 协程并发执行的方式是不可预测的。

在您提供的代码示例中,bar 函数确实会在 foo 函数之前执行,这是因为 WaitGroup 的使用方式存在问题。具体来说,wg.Add(1) 的调用时机不正确,导致主 goroutine 在等待之前,bar 的 goroutine 可能已经完成。

以下是代码的简要分析:

  • main 函数中,wg.Add(1) 被调用一次,然后启动 foo 的 goroutine。
  • foo 函数中,又调用了 wg.Add(1) 并启动 bar 的 goroutine。
  • 问题在于,wg.Add(1)foo 的 goroutine 中被调用,这可能导致主 goroutine 在 wg.Wait() 时,bar 的 goroutine 还没有被添加到 WaitGroup 中(如果 foo 的 goroutine 尚未执行到 wg.Add(1) 部分)。但在实际执行中,由于 goroutine 调度和延迟,bar 的 goroutine 可能先于 foo 的 goroutine 执行完成,导致 bar 的输出先出现。

正确的做法是在启动任何 goroutine 之前,先调用 wg.Add 来设置等待的 goroutine 数量。以下是修正后的代码示例:

package main

import (
	"fmt"
	"sync"
	"time"
)

func foo(wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Println("foo")
}

func bar(wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Println("bar")
}

func main() {
	var wg sync.WaitGroup
	wg.Add(2) // 在启动 goroutine 前添加两个等待
	go foo(&wg)
	go bar(&wg)
	wg.Wait() // 等待两个 goroutine 完成
}

在这个修正版本中:

  • 我们在 main 函数中调用 wg.Add(2),明确指定要等待两个 goroutine(foobar)。
  • 然后启动两个 goroutine,每个 goroutine 在完成时调用 wg.Done()
  • 这样,wg.Wait() 会正确等待两个 goroutine 都执行完毕,但执行顺序仍然依赖于 goroutine 调度(因此输出顺序可能不同,例如可能先输出 “bar” 后输出 “foo”,或反之)。

如果您希望控制执行顺序,需要使用其他同步机制(如通道或互斥锁),而不是依赖 WaitGroup。WaitGroup 仅用于等待一组 goroutine 完成,不保证执行顺序。

回到顶部