关于go并发编程的总结
1.使用sync进行并发:
package main
import (
"fmt"
"sync"
)
func main() {
testSlice := []string{"test1", "test2", "test3"}
wg := sync.WaitGroup{}
for _, t := range testSlice {
wg.Add(1)
go printSlice(t, &wg)
}
wg.Wait()
}
func printSlice(s string, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("this is %+v\n", s)
}
sync包实现并发的核心就是waitgroup,初始化一个WaitGroup类型的指针,需要并发时,则使用Add方法,添加计数,并在需要进行并发的函数中将其作为参数传入,当这个函数操作完成后,调用Done方法减掉计数;在主协程中,Wait方法会一直监听计数器的数量,当计数器为0时,说明所有的并发函数都完成了,这时主协程就可以退出了;个人感觉这是最简单的实现并发的方式,在需要并发处理一个集合内的所有数据时尤其好用;
2.使用管道进行并发
管道是go原生支持的数据类型,使用它也能达到并发的效果
通常的思路是,在主协程取管道中的数据,这时管道会阻塞,在发送协程中向管道里塞数据,塞完数据后关闭掉管道;当主协程取不到数据,管道也关闭后,任务就完成了
package main
import "fmt"
var channel = make(chan int, 10)
func main() {
go func() {
for i := 0; i < 10; i++ {
channel <- i
}
close(channel) //放完数据后一定要关闭chan,否则会死锁;
}()
for v := range channel {
fmt.Println(v)
} //在主协程从chan里取数据,因为取不到会一直阻塞,这样main routine就不会退出;
//如果另起一个协程取数据,在另一个协程里阻塞,但主协程并未阻塞,取数据协程还没取到,主协程就退出了;
}
上面的例子其实并没有体现出并发执行,因为十个数按次序塞进管道中,主协程按次序从管道里取出了数据,还是一个串行的过程
进阶版
package main
import (
"fmt"
)
var channel = make(chan int)
var result = make(chan int)
func main() {
go func() {
for i := 0; i < 100; i++ {
channel <- i
}
close(channel)
}()
ct := 0
for c := range channel {
go func(i int) {
result <- i + i
}(c)
}
for v := range result {
ct++
fmt.Println(v)
if ct == 100 {
break
}
}
}
把数据按次序投入到管道中后,遍历管道,每取出一个数据,则开启一个新的协程来做计算工作(i+i),然后将结果放到result队列中,最后在主协程取出result管道的值;由于需要关闭result管道,但是关闭的位置不好确定,目前暂时按计算的数据量来决定跳出循环的时机,大家也可讨论下什么时候应该关闭result管道;
注意,关闭管道与监听管道取值需要是两个不同的协程,若两个操作都在一个协程,要么监听了一个已经关闭的协程,要么监听了一个没有被关闭的协程,都会产生异常
在这里,管道的容量是0,即每次往管道塞数据需要等里面的数据被消费后才能继续塞;有容量的协程则是可以塞进数量为容量数的数据,之后的数据需要阻塞,直到有空余;
3. select关键字
先看代码
start := time.Now()
c := make(chan interface{})
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
time.Sleep(4*time.Second)
close(c)
}()
go func() {
time.Sleep(3*time.Second)
ch1 <- 3
}()
go func() {
time.Sleep(3*time.Second)
ch2 <- 5
}()
fmt.Println("Blocking on read...")
select {
case <- c:
fmt.Printf("Unblocked %v later.\n", time.Since(start))
case <- ch1:
fmt.Printf("ch1 case...")
case <- ch2:
fmt.Printf("ch2 case...")
default:
fmt.Printf("default go...")
}
运行上述代码,由于当前时间还未到3s。所以,目前程序会走default。
若注释掉default,ch1,ch2分支会随机挑选一个执行,若将ch1 ch2的sleep时间改为10秒,则会执行c分支;
即
- 如果有一个或多个关于管道操作可以完成,则Go运行时系统会随机的选择一个执行,否则的话,如果有default分支,则执行default分支语句,如果连default都没有,则select语句会一直阻塞,直到至少有一个管道操作可以进行.
- 需要有真实的goroutine存在,若所有对管道操作的的子goroutine都已退出,select{}会报panic