一个有意思的go routine案例

2022-12-07 09:09:45 浏览数 (2)

Golang 一个有意思的go routine案例

今天我们看一个有意思的go routine案例,从而了解golang中的sync包的waitgroup用法。

01

一个简单案例

我们写一个简单的for循环,循环体里面写上go routine,启动多个goroutine来打印循环变量i

代码语言:javascript复制
func main() {
    for i := 0; i < 200; i   {
        go fmt.Println("routine:", i)
        fmt.Println(i)
    }
}

上述案例可以得到下面输出:

代码语言:javascript复制
...省略
21
22
routine: 0
routine: 3
routine: 1
routine: 13
23
24
...省略
198
199
routine: 189
routine: 198
routine: 188

...

由于输出了200个数字,太长了占满页面,结果的前后我省略了一部分。

上面的例子不难看出来:

1、因为go routine是协程,所以存在这个routine的先后顺序无法保证,可能后面的循环先输出。例如routine3出现在routine1的前面。

2、go routine的运行需要时间,循环到第22次的时候,routine0的结果才输出来。循环结束的时候,还有一些go routine没有执行完。

为了保证go routine全部执行完毕并输出,我们往往会使用sleep 函数阻塞主程序main,从而等待go routine运行。上面的程序会改成:

代码语言:javascript复制
func main() {
    for i := 0; i < 200; i   {
        go fmt.Println("routine:", i)
        fmt.Println(i)
    }
    time.Sleep(time.Second * 1)
}
...

上述程序非常简单,是一个简单循环,循环体中只有打印语句,没有其他的语句。

但是如果循环中的逻辑特别多的时候,我们应该sleep多久,才能保证所有的go routine执行完毕呢???答案是不确定

02

利用channel

为了解决上述问题,我们引入了管道channel。channel默认是阻塞的。当数据被发送到channel时会发生阻塞,直到有其他go routine从该channel中读取数据。当从channel读取数据时,读取也会被阻塞,直到其他go routine将数据写入该channel。

代码语言:javascript复制
func main(){
    c := make(chan bool, 200)
    for i := 0; i < 200; i   {
        go func(i int) {
            fmt.Println(i)
            c <- true
        }(i)
    }

    for i := 0; i < 200; i   {
        <-c
    }
}

上述代码中,我们利用管道天然的阻塞特性,先初始化一个channel,channel中传递bool类型的值,然后运行go routine,并在go routine结束的时候,将true塞入channel。

在第一个循环结束之后(并非go routine结束),我们开始遍历这个channel,从channel中吐出来200个bool类型的值,如果不够200个,程序将天然阻塞,这就能够保证,所有的go routine都被执行完了。

channel的方法能够解决我们上述的time.sleep时间不确定的问题。

但是channel有一个缺点,就是比较耗费内存。假设我们的循环终止条件上限是10w或者100w,那么我们不得不申请同样大小的channel。

03

利用sync包工具sync.WaitGroup

除了channel之外,还可以使用sync.WaitGroup来解决这个问题。

WaitGroup 对象内部有一个计数器,最初从0开始,它有三个方法:Add(), Done(), Wait() 用来控制计数器的数量。其中:

Add(n) 把计数器设置为n ,

Done() 每次把计数器执行减一操作 ,

wait() 会阻塞代码的运行,直到计数器的值减为0。

代码语言:javascript复制
func main() {
    wg := sync.WaitGroup{}
    wg.Add(200)
    for i := 0; i < 200; i   {
        go func(i int) {
            fmt.Println(i)
            wg.Done()
        }(i)
    }
    wg.Wait()
} 

我们遵照上面的描述,先设置计数器为200,然后go func中执行Done,对计数器进行减一操作,然后最后利用wait函数,捕获计数器的值为0的时候,结束程序。

相比较而言,sync.WaitGroup工具会更加轻量。

0 人点赞