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

一、sync.WaitGroup

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

package main
import (
"fmt"
"sync"
)
var waitgroup sync.WaitGroup
func Afunction(shownum int) {
fmt.Println(shownum)
waitgroup.Done() //任务完成,将任务队列中的任务数量-1,其实.Done就是.Add(-1)
} func main() {
for i := ; i < ; i++ {
waitgroup.Add() //每创建一个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 main
func main() {
ch := make(chan int, )
ch <-
ch <-
ch <-
ch <- //这一行操作就会发生阻塞,因为前三行的放入数据的操作已经把channel填满了
package main
func main() {
ch := make(chan int, )
<-ch //这一行会发生阻塞,因为channel才刚创建,是空的,没有东西可以取出
}

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

ch := make(chan int) //无缓冲的channel,同等于make(chan int, 0)
ch := make(chan int, ) //一个缓冲区大小为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 <- // 输出结果:
// finish
}
package main

import "fmt"

func Afuntion(ch chan int) {
fmt.Println("finish")
<-ch
} func main() {
ch := make(chan int) //无缓冲的channel
//只是把这两行的代码顺序对调一下
ch <-
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, )
for i := ; i < ; i++ {
//每当创建goroutine的时候就向channel中放入一个数据,如果里面已经有10个数据了,就会
//阻塞,由此我们将同时运行的goroutine的总数控制在<=10个的范围内
ch <-
go Afunction(ch)
}
// 这里只是示范个例子,当然,接下来应该有些更加周密的同步操作
}

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

package main
func Afunction(ch chan int) {
ch <-
ch <-
ch <-
ch <-
ch <- <-ch
} func main() {
//主routine的操作同上面那段代码
ch := make(chan int, )
for i := ; i < ; i++ {
ch <-
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 main
func main() {
ch := make(chan int, )
ch <-
ch <-
close(ch)
ch <- //不能对关闭的channel执行放入操作 // 会触发panic
}
package main
func main() {
ch := make(chan int, )
ch <-
ch <-
close(ch)
<-ch //只要channel还有数据,就可能执行取出操作 //正常结束
}
package main

import "fmt"

func main() {
ch := make(chan int, )
ch <-
ch <-
ch <-
ch <-
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 main
func Afunction(ch chan int) {
ch <-
} func main() {
var (
ch chan int = make(chan int, ) //可以同时运行的routine数量为20
dutycount int =
)
for i := ; i < dutycount; i++ {
go Afunction(ch)
} //知道了任务总量,可以像这样利用固定循环次数的循环检测所有的routine是否工作完毕
for i := ; 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, )
feedback chan string = make(chan string, ) msg string
allwork int
finished int
)
for i := ; i < ; i++ {
routineCtl <-
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

Golang 并发Groutine实例解读(二)的更多相关文章

  1. Golang 并发Groutine实例解读(一)

    Go语言的并发和并行 不知道你有没有注意到一个现象,还是这段代码,如果我跑在两个goroutines里面的话: var quit chan int = make(chan int) func loop ...

  2. Golang 并发Groutine详解

    概述 1.并行和并发 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行. 并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在 ...

  3. golang并发编程

    golang并发编程 引子 golang提供了goroutine快速实现并发编程,在实际环境中,如果goroutine中的代码要消耗大量资源时(CPU.内存.带宽等),我们就需要对程序限速,以防止go ...

  4. SQL Server 2008 数据库镜像部署实例之二 配置镜像,实施手动故障转移

    SQL Server 2008 数据库镜像部署实例之二 配置镜像,实施手动故障转移 上一篇文章已经为配置镜像数据库做好了准备,接下来就要进入真正的配置阶段 一.在镜像数据库服务器上设置安全性并启动数据 ...

  5. Selenium2学习-022-WebUI自动化实战实例-020-JavaScript 在 Selenium 自动化中的应用实例之二(获取浏览器显示区域大小)

    前几篇文章中简略概述了,如何获取.设置浏览器窗口大小,那么我们该如何获取浏览器显示区域的大小呢?此文讲对此进行简略概述,敬请各位小主参阅.若有不足之处,敬请各位大神指正,不胜感激! 获取浏览器显示区域 ...

  6. 【WCF--初入江湖】08 并发与实例模式

    08 并发与实例模式 1. 实例上下文模式   一个服务代理:servicePoxy ChannelFactory<IService1> factoryservicel = new Cha ...

  7. python学习_数据处理编程实例(二)

    在上一节python学习_数据处理编程实例(二)的基础上数据发生了变化,文件中除了学生的成绩外,新增了学生姓名和出生年月的信息,因此将要成变成:分别根据姓名输出每个学生的无重复的前三个最好成绩和出生年 ...

  8. SonarQube4.4+Jenkins进行代码检查实例之二

    SonarQube4.4+Jenkins进行代码检查实例之二 SonarQube4.4+Jenkins进行代码检查实例之二

  9. 一些有用的javascript实例分析(二)

    原文:一些有用的javascript实例分析(二) 5 求出数组中所有数字的和 window.onload = function () { var oBtn = document.getElement ...

随机推荐

  1. redis开机自启动脚本(linux)

    目前redis放在home下的文件夹中,写一个脚本,待系统启动的过程中,去启动该脚本. 脚本:redis.sh #!/bin/sh /home/juepei/Downloads/redis-3.0.0 ...

  2. Web app制作细节:web app互动制作技巧

    Google .微软.苹果三大巨头紧锣密鼓地在web app的研发产品领域圈地设岗,并试图建立以自己为中心的”云“服务平台,企图在web app时代到来的时候充当霸主.本文将围绕web app的制作, ...

  3. android应用搬家的实现

    android手机上的应用搬家功能,具体的介绍和原理参考: 系统目录及应用搬家的研究 应用搬家的实现 这里主要是作为一个补充,因为上面两篇文章虽然讲的挺好的,但是给出的例子不能直接运行,还是需要一些准 ...

  4. 记一次golang的实践

    之前做过一个深交所股票数据的接存储软件,消息的协议是这样. 协议文档在这  https://wenku.baidu.com/view/d102cd0b4a73f242336c1eb91a37f111f ...

  5. vs installer 将.net framework 集成到安装包中

    Missing .NET Framework 4.0 in Visual Studio 2017 Prerequisites whenhttps://stackoverflow.com/questio ...

  6. 使用session的监听器获取当前在线人数

    1首先在web.xml中配置Session的监听器 2创建监听器并且继承HttpSessionListener 3.在jsp中导入监听器 4.获取当前在线人数 5.配置到公共网络(使用natapp的免 ...

  7. Elasticsearch安装与环境配置

    Elasticsearch安装与环境配置 确保机器上已经安装了jdk7以上版本 下载:官网下载地址:https://www.elastic.co/downloads/elasticsearch 将下载 ...

  8. Struts2中使用OGNL表达式语言访问静态方法和静态属性

    1.一定要在struts.xml配置文件中配置允许使用OGNL访问静态方法,否则不能使用OGNL访问静态方法 2.<s:property value="@com.gk.StaticTe ...

  9. MySQL 高级查询操作

    目录 MySQL 高级查询操作 一.预告 二.简单查询 三.显示筛选 四.存储过程 五.查询语句 1.作为变量 2.函数调用 3.写入数据表 备注 附表一 附表二 相关文献 博客提示 MySQL 高级 ...

  10. Java Maven项目的一些补充

    写在前面 在多人开发团队中,规范是非常重要的.To explain it,就像是秦始皇统一货币.度量衡一样,主要的目的是为了降低项目技术架构差异所带来的交流成本,这样开发者就可以尽可能把注意力放在业务 ...