博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Golang 并发Groutine实例解读(二)
阅读量:5010 次
发布时间:2019-06-12

本文共 6800 字,大约阅读时间需要 22 分钟。

go提供了sync包和channel机制来解决协程间的同步与通信。

一、sync.WaitGroup

sync包中的WaitGroup实现了一个类似任务队列的结构,你可以向队列中加入任务,任务完成后就把任务从队列中移除,如果队列中的任务没有全部完成,队列就会触发阻塞以阻止程序继续运行,具体用法参考如下代码:

package mainimport (    "fmt"    "sync")var waitgroup sync.WaitGroupfunc Afunction(shownum int) {    fmt.Println(shownum)    waitgroup.Done() //任务完成,将任务队列中的任务数量-1,其实.Done就是.Add(-1)} func main() {    for i := 0; i < 10; i++ {        waitgroup.Add(1) //每创建一个goroutine,就把任务队列中任务的数量+1        go Afunction(i)    }    waitgroup.Wait() //.Wait()这里会发生阻塞,直到队列中所有的任务结束就会解除阻塞}

我们可以利用sync.WaitGroup来满足这样的情况:

        ▲某个地方需要创建多个goroutine,并且一定要等它们都执行完毕后再继续执行接下来的操作。

是的,WaitGroup最大的优点就是.Wait()可以阻塞到队列中的任务都完毕后才解除阻塞。

二、channel

channel是一种golang内置的类型,英语的直译为"通道",其实,它真的就是一根管道,而且是一个先进先出的数据结构。

我们能对channel进行的操作只有4种:

(1) 创建chennel (通过make()函数)

(2) 放入数据 (通过 channel <- data 操作) 

(3) 取出数据 (通过 <-channel 操作)

(4)  关闭channel (通过close()函数)

但是channel有一些非常给力的性质需要你牢记,请一定要记住并理解好它们:

(1) channel是一种阻塞管道,是自动阻塞的。意思就是,如果管道满了,一个对channel放入数据的操作就会阻塞,直到有某个routine从channel中取出数据,这个放入数据的操作才会执行。相反同理,如果管道是空的,一个从channel取出数据的操作就会阻塞,直到某个routine向这个channel中放入数据,这个取出数据的操作才会执行。这是channel最重要的一个性质,没有之一。

package mainfunc main() {    ch := make(chan int, 3)    ch <- 1    ch <- 1    ch <- 1    ch <- 1 //这一行操作就会发生阻塞,因为前三行的放入数据的操作已经把channel填满了
package mainfunc main() {    ch := make(chan int, 3)    <-ch //这一行会发生阻塞,因为channel才刚创建,是空的,没有东西可以取出}

(2)channel分为有缓冲的channel和无缓冲的channel。两种channel的创建方法如下:

ch := make(chan int) //无缓冲的channel,同等于make(chan int, 0)ch := make(chan int, 5) //一个缓冲区大小为5的channel

操作一个channel时一定要注意其是否带有缓冲,因为有些操作会触发channel的阻塞导致死锁。下面就来解释这些需要注意的情景。

首先来看一个一个例子,这个例子是两段只有主函数不同的代码:

package main import "fmt" func Afuntion(ch chan int) {    fmt.Println("finish")    <-ch} func main() {    ch := make(chan int) //无缓冲的channel    go Afuntion(ch)    ch <- 1        // 输出结果:    // finish}
package main import "fmt" func Afuntion(ch chan int) {    fmt.Println("finish")    <-ch} func main() {    ch := make(chan int) //无缓冲的channel    //只是把这两行的代码顺序对调一下    ch <- 1    go Afuntion(ch)     // 输出结果:    // 死锁,无结果}

前一段代码最终会输出"finish"并正常结束,但是后一段代码会发生死锁。为什么会出现这种现象呢,咱们把上面两段代码的逻辑跑一下。

 

第一段代码:

        1. 创建了一个无缓冲channel

        2. 启动了一个goroutine,这个routine中对channel执行取出操作,但是因为这时候channel为空,所以这个取出操作发生阻塞,但是主routine可没有发生阻塞,它还在继续运行呢

        3. 主goroutine这时候继续执行下一行,往channel中放入了一个数据

        4. 这时阻塞的那个routine检测到了channel中存在数据了,所以接触阻塞,从channel中取出数据,程序就此完毕

 

第二段代码:

        1.  创建了一个无缓冲的channel

        2.  主routine要向channel中放入一个数据,但是因为channel没有缓冲,相当于channel一直都是满的,所以这里会发生阻塞。可是下面的那个goroutine还没有创建呢,主routine在这里一阻塞,整个程序就只能这么一直阻塞下去了,然后。。。然后就没有然后了。。死锁!

 

※从这里可以看出,对于无缓冲的channel,放入操作和取出操作不能再同一个routine中,而且应该是先确保有某个routine对它执行取出操作,然后才能在另一个routine中执行放入操作。

 

对于带缓冲的channel,就没那么多讲究了,因为有缓冲空间,所以只要缓冲区不满,放入操作就不会阻塞,同样,只要缓冲区不空,取出操作就不会阻塞。而且,带有缓冲的channel的放入和取出可以用在同一个routine中。

但是,并不是说有了缓冲就可以随意使用channel的放入和取出了,我们一定要注意放入和取出的速率问题。下面我们就举个例子来说明这种问题:

我们经常会用利用channel自动阻塞的性质来控制当前运行的goroutine的总数量,如下:

package main import (    "fmt") func Afunction(ch chan int) {    fmt.Println("finish")    <-ch //goroutine执行完了就从channel取出一个数据} func main() {    ch := make(chan int, 10)    for i := 0; i < 1000; i++ {        //每当创建goroutine的时候就向channel中放入一个数据,如果里面已经有10个数据了,就会        //阻塞,由此我们将同时运行的goroutine的总数控制在<=10个的范围内        ch <- 1        go Afunction(ch)    }    // 这里只是示范个例子,当然,接下来应该有些更加周密的同步操作}

上面这种channel的使用方式几乎经常会用到,但是再看一下接下来这段代码,它和上面这种使用channel的方式几乎一样,但是它会造成问题:

package mainfunc Afunction(ch chan int) {    ch <- 1    ch <- 1    ch <- 1    ch <- 1    ch <- 1     <-ch} func main() {    //主routine的操作同上面那段代码    ch := make(chan int, 10)    for i := 0; i < 100; i++ {        ch <- 1        go Afunction(ch)    }     // 这段代码运行的结果为死锁}

上面这段运行和之前那一段基本上原理是一样的,但是运行后却会发生死锁。为什么呢?其实总结起来就一句话,"放得太快,取得太慢了"。

按理说,我们应该在我们主routine中创建子goroutine并每次向channel中放入数据,而子goroutine负责从channel中取出数据。但是我们的这段代码在创建了子goroutine后,每个routine会向channel中放入5个数据。这样,每向channel中放入6个数据才会执行一次取出操作,这样一来就可能会有某一时刻,channel已经满了,但是所有的routine都在执行放入操作(因为它们当前执行放入操作的概率是执行取出操作的6倍),这样一来,所有的routine都阻塞了,从而导致死锁。

在使用带缓冲的channel时一定要注意放入与取出的速率问题。

 (3)关闭后的channel可以取数据,但是不能放数据。而且,channel在执行了close()后并没有真的关闭,channel中的数据全部取走之后才会真正关闭。

package mainfunc main() {    ch := make(chan int, 5)    ch <- 1    ch <- 1    close(ch)    ch <- 1 //不能对关闭的channel执行放入操作                // 会触发panic}
package mainfunc main() {    ch := make(chan int, 5)    ch <- 1    ch <- 1    close(ch)    <-ch //只要channel还有数据,就可能执行取出操作         //正常结束}
package main import "fmt" func main() {    ch := make(chan int, 5)    ch <- 1    ch <- 1    ch <- 1    ch <- 1    close(ch)  //如果执行了close()就立即关闭channel的话,下面的循环就不会有任何输出了    for {        data, ok := <-ch        if !ok {            break        }        fmt.Println(data)    }        // 输出:    // 1    // 1    // 1    // 1    //     // 调用了close()后,只有channel为空时,channel才会真的关闭}

 

三、使用channel控制goroutine数量

channel的性质到这里就介绍完了,但是看上去,channel的使用似乎比WaitGroup要注意更多的细节,那么有什么理由一定要用channel来实现同步呢?channel相比WaitGroup有一个很大的优点,就是channel不仅可以实现协程的同步,而且可以控制当前正在运行的goroutine的总数。

下面就介绍几种利用channel控制goroutine数量的方法:

1.如果任务数量是固定的:

ackage mainfunc Afunction(ch chan int) {    ch <- 1} func main() {    var (        ch        chan int = make(chan int, 20) //可以同时运行的routine数量为20        dutycount int      = 500    )    for i := 0; i < dutycount; i++ {        go Afunction(ch)    }     //知道了任务总量,可以像这样利用固定循环次数的循环检测所有的routine是否工作完毕    for i := 0; i < dutycount; i++ {        <-ch    }}

2.如果任务的数量不固定

package main import (    "fmt") func Afunction(routineControl chan int, feedback chan string) {    defer func() {        <-routineControl        feedback <- "finish"    }()     // do some process    // ...} func main() {    var (        routineCtl chan int    = make(chan int, 20)        feedback   chan string = make(chan string, 10000)         msg      string        allwork  int        finished int    )    for i := 0; i < 1000; i++ {        routineCtl <- 1        allwork++        go Afunction(routineCtl, feedback)    }     for {        msg = <-feedback        if msg == "finish" {            finished++        }        if finished == allwork {            break        }    }}

 

四、不要使用无限循环检查goroutine是否完成工作

 在使用goroutine时,我们经常会写出这样的代码:

package main import (    "fmt") var (    flag bool    str  string) func foo() {    flag = true    str = "setup complete!"} func main() {    go foo()    for !flag {        //按照我们的本意,foo()执行完毕后,flag=true,循环就会退出。        //但是其实这个循环永远都不会退出    }    fmt.Println(str)}

运行之后发现main中的无限循环永远也无法退出,所以Go中不要用这种无限轮询的方式来检查goroutine是否完成了工作。

 

我们可以通过使用channel,让foo()和main()实现通信,让foo()执行完毕后通过channel发送一个消息给main(),告诉它自己的事儿完成了,然后main()收到消息后继续执行其他操作:

package main import (    "fmt") var (    flag bool    str  string) func foo(ch chan string) {    flag = true    str = "setup complete!"    ch <- "I'm complete." //foo():我的任务完成了,发个消息给你~} func main() {    ch := make(chan string)    go foo(ch)    <-ch //main():OK,收到你的消息了~    for !flag {    }    fmt.Println(str)}

 

 

 

 

 

本文转自:http://blog.csdn.net/gophers/article/details/24665419

转载于:https://www.cnblogs.com/liuzhongchao/p/9633814.html

你可能感兴趣的文章
网络编程-socket并发-粘包问题
查看>>
python 中安装pandas
查看>>
Hibernate 的<generator class="native"></generator>的不同属性含义
查看>>
linux修改root账户的用户名所得的教训
查看>>
【LeetCode】Flatten Binary Tree to Linked List
查看>>
读后感-浮生六纪
查看>>
执行指定路径的程序文件
查看>>
Leetcode-950 Reveal Cards In Increasing Order(按递增顺序显示卡牌)
查看>>
[Linux] 在 Linux CLI 使用 ssh-keygen 生成 RSA 密钥
查看>>
14款下载有用脚本的超酷网站
查看>>
LXC-Linux Containers介绍
查看>>
7.31实习培训日志-docker sql
查看>>
c#中使用servicestackredis操作redis
查看>>
ios app 真机crash报告分析
查看>>
CRC标准以及简记式
查看>>
SEO搜索引擎
查看>>
关于本地使用tomcat部署web应用,浏览器自动跳转为https的问题
查看>>
一、Text To Speech
查看>>
Java读取并下载网络文件
查看>>
github上构建自己的个人网站
查看>>