关于go并发编程的总结
2013 年 8 月 10 日
关于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 } } }
注意,关闭管道与监听管道取值需要是两个不同的协程,若两个操作都在一个协程,要么监听了一个已经关闭的协程,要么监听了一个没有被关闭的协程,都会产生异常
在这里,管道的容量是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。
即
- 如果有一个或多个关于管道操作可以完成,则Go运行时系统会随机的选择一个执行,否则的话,如果有default分支,则执行default分支语句,如果连default都没有,则select语句会一直阻塞,直到至少有一个管道操作可以进行.
- 需要有真实的goroutine存在,若所有对管道操作的的子goroutine都已退出,select{}会报panic