并发控制得心应手
上一节提到,golang中直接使用关键字go
创建goroutine
,无法满足我们的需求。主要问题如下
- 无法有效的并发执行完成
- 无法有效的控制并发
首先我们再来看看下面这个栗子,代码如下
1// example-goroutine anonymous 2 3package main 4 5import ( 6 "fmt" 7 "time" 8) 9 10func anonymous1() { 11 startTime := time.Now() 12 // 创建十个goroutine 13 for i := 0; i < 10; i++ { 14 go func() { 15 fmt.Println("HelloWord~, stamp is", i) 16 }() 17 } 18 fmt.Println("Main~") 19 spendTime := time.Since(startTime) 20 fmt.Println("Spend Time:", spendTime) 21 // 防止goroutine提前退出 22 // time.Sleep(time.Second) 23} 24 25// goroutine anonymous 26func main() { 27 anonymous2() 28}
此时你会发现有些任务被多次执行了,但有些任务却又没有被执行。以上例子虽加速了运行,但带来的损失却也是巨大的。例如银行转账等,一旦出现以上情况多次付款也随之而来了。弊大于利
首先我们来分析以上代码,为什么会出现此种情况?虽然是个废品,但也是俺辛辛苦苦的写的不是,让俺做个明白鬼。
我们从里面往外分析anonymous1
首先他是个匿名函数 + 立即执行函数,且变量i
并不是传递的参数,而是外部循环带进来的。由上图,我们知道,执行流程为先创建
goroutine,执行逻辑,返回结果。
请思考:
goroutine,越多越好么?为什么
如何避免以上情景?如何避免提前退出?
信道-Channel
信道的英文是channel,在golang当中的关键字是chan。它的用途是用来在goroutine之间传输数据,这里你可能要问了,为什么一定得是goroutine之间传输数据呢,函数之间传递不行吗?
因为正常的传输数据直接以参数的形式传递就可以了,只有在并发场景当中,多个线程彼此隔离的情况下,才需要一个特殊的结构传输数据。
Go语言的并发模型是CSP(Communicating Sequential Processes)
,提倡通过通信共享内存而不是通过共享内存而实现通信。
如果说goroutine
是Go程序并发的执行体,channel
就是它们之间的连接。channel
是可以让一个goroutine
发送特定值到另一个goroutine
的通信机制。
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
channel底层的实现为互斥锁
example
1var 变量 chan 元素类型 2// example-var 3// 只声明 4var a chan int 5var b chan string 6var c chan byte 7var d chan []string 8var e chan []int 9// 实例化 10a = make(chan []int) 11 12//example-2(推荐使用) 13管道名称 := make(chan 数据类型 [缓冲区size])
无缓冲channel
示例代码如下
1package main 2 3import ( 4 "fmt" 5 "time" 6) 7 8func hello(intCh <-chan int) { 9 fmt.Println("Hello, Gopher. I am stamp[Id]", <-intCh) 10 time.Sleep(time.Second * 2) 11} 12 13func main() { 14 startTime := time.Now() 15 const jobNumber = 100 * 100 16 // create chan 17 intCh := make(chan int) 18 for i := 0; i <= jobNumber; i++ { 19 // create goroutine same number for jobNumber 20 go hello(intCh) 21 intCh <- i 22 } 23 fmt.Println("Completed, Spend Time :", time.Since(startTime)) 24}
这速度可谓是非常的快啊
带缓冲Channel
带缓冲的 channel(buffered channel) 是一种在被接收前能存储一个或者多个值的通道。这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:
无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证
来段代码压压惊
1package main 2 3import ( 4 "fmt" 5 "time" 6) 7 8func hello(intCh <-chan int) { 9 fmt.Println("Hello, Gopher. I am stamp[Id]", <-intCh) 10 time.Sleep(time.Second * 2) 11} 12 13func hello1(intCh <-chan int) { 14 fmt.Println("Hello, Gopher1. I am stamp[Id]", <-intCh) 15 time.Sleep(time.Second * 2) 16} 17 18func main() { 19 startTime := time.Now() 20 const jobNumber = 100 * 100 21 // create chan 22 intCh := make(chan int, 100) 23 for i := 0; i <= jobNumber; i++ { 24 // create goroutine same number for jobNumber 25 go hello(intCh) 26 go hello1(intCh) 27 intCh <- i 28 } 29 fmt.Println("Completed, Spend Time :", time.Since(startTime)) 30}
运行效果如下
这速度杠杠滴哈,别急,同时也让我和你说执行流程,老规矩,上图
首先开始执行把需要传递的数据打到channle里面,然后goroutine去取,执行。那么有留下几个问题
还可以加速么?
加速的方法?
可能带来什么新的问题?
如何解决?
单向 channel
有时候,我们有一些特殊的业务需求,比如限制一个 channel 只可以接收但是不能发送,或者限制一个 channel 只能发送但不能接收,这种 channel 称为单向 channel。
单向 channel 的声明也很简单,只需要在声明的时候带上 <- 操作符即可,如下面的代码所示:
1onlySend := make(chan<- int) 2onlyReceive:=make(<-chan int)
使用单向 channel 的较多场景一般在函数参数中使用较多,这样可以防止一些操作影响了 channel。
1//example channel 2onlySend := make(chan<- int) 3onlyReceive:=make(<-chan int) 4 5//example function 6package main 7 8import "fmt" 9 10func exs(accept <-chan int, recipient chan<- int) { 11 for result := range accept { 12 fmt.Println("Received only sent channel a:", result) 13 recipient <- result + 2 14 } 15 16 //fmt.Println("Send Only", recipient) 17} 18 19func main() { 20 const processNumber = 100 21 sender := make(chan int, processNumber) 22 recipient := make(chan int, processNumber) 23 for e := 0; e < 10; e++ { 24 go exs(sender, recipient) 25 } 26 27 for s := 0; s < processNumber; s++ { 28 sender <- s 29 } 30 for r := 0; r < processNumber; r++ { 31 //<-recipient 32 fmt.Println("recipient", <-recipient) 33 } 34}
小技巧:箭头该谁指谁?这可把我整的不好了,别慌,我告诉你,到底该谁指谁。其实很简单
箭头一致向左指
1a <- chan <- b 2 3// 存入 4chan <- b 5 6// 取出 7a := <- chan
Chan其实就是起到一个中间人的作用,箭头指向chan,那就是放入,chan指出去 就是拿出来。
相信你应该记住了吧,反正我记住了
多路复用Channel
假设要从网上下载一个文件,启动了 5个 goroutine 进行下载,并把结果发送到 5 个 channel 中。其中,哪个先下载好,就会使用哪个 channel 的结果。
在这种情况下,如果我们尝试获取第一个 channel 的结果,程序就会被阻塞,无法获取剩下4个 channel 的结果,也无法判断哪个先下载好。这个时候就需要用到多路复用操作了,在 Go 语言中,通过 select 语句可以实现多路复用,其语句格式如下:
1select{ 2 case <-ch1: 3 ... 4 case data := <-ch2: 5 ... 6 case ch3<-data: 7 ... 8 default: 9 默认操作 10}
整体结构和 switch 非常像,都有 case 和 default,只不过 select 的 case 是一个个可以操作的 channel。
小提示:多路复用可以简单地理解为,N 个 channel 中,任意一个 channel 有数据产生,select 都可以监听到,然后执行相应的分支,接收数据并处理。
使用
select
语句能提高代码的可读性。
- 可处理一个或多个channel的发送/接收操作。
- 如果多个
case
同时满足,select
会随机选择一个。
- 对于没有
case
的select{}
会一直等待,可用于阻塞main函数。
1// example select 2package main 3 4import ( 5 "fmt" 6 "time" 7) 8 9func main() { 10 11 c1 := make(chan string) 12 c2 := make(chan string) 13 14 go func() { 15 time.Sleep(1 * time.Second) 16 c1 <- "one" 17 }() 18 go func() { 19 time.Sleep(2 * time.Second) 20 c2 <- "two" 21 }() 22 23 for i := 0; i < 2; i++ { 24 select { 25 case msg1 := <-c1: 26 fmt.Println("received", msg1) 27 case msg2 := <-c2: 28 fmt.Println("received", msg2) 29 } 30 } 31}
小结:关于数据流动、传递等情况的优先使用channle
, 它是并发安全的,且性能优异
Sync深入并发控制
sync.waitGroup
在此之前我们先去,解决一个开启goroutine
,提前退出的例子
示例代码如下
1package main 2 3import ( 4 "fmt" 5 "sync" 6 //"time" 7) 8 9var wg sync.WaitGroup 10func main() { 11 for i := 0; i < 10 ; i++ { 12 go exampleOut(i) 13 } 14} 15 16func exampleOut(i int) { 17 fmt.Println("Hello, Gopher, I am [Id]", i) 18}
仔细看,你会发现根本就没有输出,原因是它开启goroutine
,也需要时间。main函数并会等待,当然我们也可以手动添加一个停止,但这个并不能有效的阻止(你我都知道需要多久才能把goroutine
执行完成),那有没有办法。。。
答案当然是有滴,它就是sync.WaitGroup