蓝田网站建设,手机怎么同步连接wordpress,制作投票网站,淘宝关键词搜索工具怎样才能让主goroutine等待其他goroutine#xff1f;
上篇文章提到#xff0c;一旦主 goroutine 中的代码执行完毕#xff0c;当前的 Go 程序就会结束运行#xff0c;无论其他的 goroutine 是否已经在运行了。那么#xff0c;怎样才能做到等其他的 goroutine 运行完毕之后…怎样才能让主goroutine等待其他goroutine
上篇文章提到一旦主 goroutine 中的代码执行完毕当前的 Go 程序就会结束运行无论其他的 goroutine 是否已经在运行了。那么怎样才能做到等其他的 goroutine 运行完毕之后再让主 goroutine 结束运行呢
最简单粗暴的办法就是让主goroutine小睡一会儿。
for i : 0; i 10; i {
go func() {fmt.Println(i)}()
}
time.Sleep(time.Millisecond * 500)在for语句的后边调用了time包的Sleep函数并把time.Millisecond * 500的结果作为参数值传给了它。time.Sleep函数的功能就是让当前的 goroutine在这里就是主 goroutine暂停运行一段时间直到到达指定的恢复运行时间。
time.Sleep函数会在被调用时用当前的绝对时间再加上相对时间计算出在未来的恢复运行时间。显然一旦到达恢复运行时间当前的 goroutine 就会从“睡眠”中醒来并开始继续执行后边的代码。
但是让主 goroutine“睡眠”多长时间才是合适的呢 如果“睡眠”太短则很可能不足以让其他的 goroutine 运行完毕而若“睡眠”太长则纯属浪费时间这个时间就太难把握了。
我们可以创建一个通道它的长度应该与我们手动启用的 goroutine的数量一致。在每个手动启用的 goroutine 即将运行完毕的时候我们都要向该通道发送一个值。
package mainimport (fmt//time
)func main() {num : 10sign : make(chan struct{}, num)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}
}声明通道sign的时候是以chan struct{}作为其类型的。其中的类型字面量struct{}有些类似于空接口类型interface{}它代表了既不包含任何字段也不拥有任何方法的空结构体类型。
注意struct{}类型值的表示法只有一个即struct{}{}。并且它占用的内存空间是0字节。确切地说这个值在整个 Go 程序中永远都只会存在一份。虽然我们可以无数次地使用这个值字面量但是用到的却都是同一个值。当我们仅仅把通道当作传递某种简单信号的介质的时候用struct{}作为其元素类型是再好不过的了。
有没有比使用通道更好的方法如果你知道标准库中的代码包sync的话那么可能会想到sync.WaitGroup类型。没错这是一个更好的答案。
怎样让我们启用的多个goroutine按照既定的顺序运行
首先我们需要稍微改造一下for语句中的那个go函数要让它接受一个int类型的参数并在调用它的时候把变量i的值传进去。为了不改动这个go函数中的其他代码我们可以把它的这个参数也命名为i。
for i : 0; i 10; i {go func(i int) {fmt.Println(i)
}(i)
}在go语句被执行时我们传给go函数的参数i会先被求值如此就得到了当次迭代的序号。之后无论go函数会在什么时候执行这个参数值都不会变。也就是说go函数中调用的fmt.Println函数打印的一定会是那个当次迭代的序号。
然后我们再改造for语句中的go函数。
for i : uint32(0); i 10; i {go func(i uint32) {fn : func() {fmt.Println(i)}trigger(i, fn)}(i)
}在go函数中先声明了一个匿名的函数并把它赋给了变量fn。这个匿名函数做的事情很简单只是调用fmt.Println函数以打印go函数的参数i的值。
在这之后我调用了一个名叫trigger的函数并把go函数的参数i和刚刚声明的变量fn作为参数传给了它。注意for语句声明的局部变量i和go函数的参数i的类型都变了都由int变为了uint32。
再来说trigger函数。该函数接受两个参数一个是uint32类型的参数i, 另一个是func()类型的参数fn。你应该记得func()代表的是既无参数声明也无结果声明的函数类型。
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。除非发现条件已满足否则它会不断地进行检查。
最后要说的是因为我依然想让主 goroutine 最后一个运行完毕所以还需要加一行代码。
trigger(10, func(){})调用trigger函数完全可以达到相同的效果。由于当所有手动启用的goroutine都运行完毕之后count的值一定会是10所以我就把10作为了第一个参数值。又由于我并不想打印这个10所以我把一个什么都不做的函数作为了第二个参数值。
完整代码
package mainimport (fmtsync/atomictime
)func main() {var count uint32trigger : 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() {})
}文章学习自郝林老师的《Go语言36讲》