Golang 并发Groutine实例解读(二)
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实例解读(二)的更多相关文章
- Golang 并发Groutine实例解读(一)
Go语言的并发和并行 不知道你有没有注意到一个现象,还是这段代码,如果我跑在两个goroutines里面的话: var quit chan int = make(chan int) func loop ...
- Golang 并发Groutine详解
概述 1.并行和并发 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行. 并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在 ...
- golang并发编程
golang并发编程 引子 golang提供了goroutine快速实现并发编程,在实际环境中,如果goroutine中的代码要消耗大量资源时(CPU.内存.带宽等),我们就需要对程序限速,以防止go ...
- SQL Server 2008 数据库镜像部署实例之二 配置镜像,实施手动故障转移
SQL Server 2008 数据库镜像部署实例之二 配置镜像,实施手动故障转移 上一篇文章已经为配置镜像数据库做好了准备,接下来就要进入真正的配置阶段 一.在镜像数据库服务器上设置安全性并启动数据 ...
- Selenium2学习-022-WebUI自动化实战实例-020-JavaScript 在 Selenium 自动化中的应用实例之二(获取浏览器显示区域大小)
前几篇文章中简略概述了,如何获取.设置浏览器窗口大小,那么我们该如何获取浏览器显示区域的大小呢?此文讲对此进行简略概述,敬请各位小主参阅.若有不足之处,敬请各位大神指正,不胜感激! 获取浏览器显示区域 ...
- 【WCF--初入江湖】08 并发与实例模式
08 并发与实例模式 1. 实例上下文模式 一个服务代理:servicePoxy ChannelFactory<IService1> factoryservicel = new Cha ...
- python学习_数据处理编程实例(二)
在上一节python学习_数据处理编程实例(二)的基础上数据发生了变化,文件中除了学生的成绩外,新增了学生姓名和出生年月的信息,因此将要成变成:分别根据姓名输出每个学生的无重复的前三个最好成绩和出生年 ...
- SonarQube4.4+Jenkins进行代码检查实例之二
SonarQube4.4+Jenkins进行代码检查实例之二 SonarQube4.4+Jenkins进行代码检查实例之二
- 一些有用的javascript实例分析(二)
原文:一些有用的javascript实例分析(二) 5 求出数组中所有数字的和 window.onload = function () { var oBtn = document.getElement ...
随机推荐
- 浏览器中调用PHP在执行linux sudo指令时报sudo: sorry, you must have a tty to run sudo
在php程序中使用了exec函数调用sudo指令,在浏览器中访问后,报sudo: sorry, you must have a tty to run sudo错误. 按照网上搜到的方法,修改/etc/ ...
- python 实现九型人格测试小程序
用python实现九型人格测试,并把测试结果绘制成饼图,实现代码如下: # @Description: 九型人格 import xlrd, matplotlib.pyplot as plt data ...
- c++ 日志输出库 spdlog 简介(4)- 多线程txt输出日志
在上一节的代码中加入了向文本文件中写入日志的代码: UINT CMFCApplication1Dlg::Thread1(LPVOID pParam) { try{ size_t q_size = ; ...
- CC2530学习路线-基础实验-GPIO 控制LED灯亮灭(1)
目录 1.前期预备知识 1.1 新大陆ZigBee模块LED灯电路 1.2 CC2530相关寄存器 1.3 寄存器操作技巧 1.4 CPU空转延时 1.4 操作流程图 2.程序代码 The End 1 ...
- s11 day100路飞项目逻辑购物车一
Luffy项目 先看练习,如下: 一. 添加购物车和查看 1. url url(r'^shoppingcar/$', shoppingcar.ShoppingCarView.as_view({&quo ...
- Pyplot绘图的格式
字符 颜色 ‘b’ 蓝色,blue ‘g’ 绿色,green ‘r’ 红色,red ‘c’ 青色,cyan ‘m’ 品红,magenta ‘y’ 黄色,yellow ‘k’ 黑色,black ‘w’ ...
- Myeclipse中java项目转成Web项目
在eclipse导入一个myeclipse建的web项目后,在Eclipse中显示的还是java项目,按下面的步骤可以将其转换成web项目. 1.找到项目目录下的.project文件 2.编辑.pro ...
- Windows下安装MySQL详细教程
Windows下安装MySQL详细教程 1.安装包下载 2.安装教程 (1)配置环境变量 (2)生成data文件 (3)安装MySQL (4)启动服务 (5)登录MySQL (6)查询用户密码 (7 ...
- django 的ajax 请求,使用form的验证机制。
所有的form都需要在后台验证,前台验证是不可靠的,django的验证是后台验证,前台提示错误信息. js验证是在前台的,无需发送消息给后台,但安全性不可靠,强调的是用户体验. 要求,使用弹出框,弹出 ...
- Oracle 数据库维护管理之--数据库基本信息表管理与优化参考1
1.查看当前系统中的会话(如果权限不足,请使用sys或者system用户登录): select * from v$session t; 2.查看此会话下正在执行的sql语句:select sql_te ...