上篇文章go 协程,我们介绍了go协程以及channel的简单用法,这篇我们详细介绍go channel的一些高级用法。
通道选择器select
说起channal的高级用法,我们必须先介绍下通道选择器select,它让你同时等待多个通道操作, 类似于用于通信的 switch 语句。 每个 case 必须是一个通信操作,要么是发送要么是接收。 select 随机执行一个可运行的 case。 如果没有 case 可运行,它将阻塞,直到有 case 可运行。 一个默认的子句应该总是可运行的。/* 语法如下如果任意某个通信可以进行,它就执行,其他被忽略如果有多个case都可以运行,select会随机地选出一个执行。其他不会执行如果没有case可以运行: - 如果有default子句,则执行该语句 - 如果没有default子句,select将阻塞,直到某个通信可以运行*/select { case channel1: exec1() case channel2: exec2() // 可以定义任意数量的 case default : // 可选 execn()}
与select配合,可以产生很多高级用法。
1,协程间同步等待
默认通道是无缓冲的,发送和接收操作是阻塞的,直到发送方和接收方都准备完毕,我们可以利用这一特性,实现协程间的同步等待。package mainimport "fmt"// ch1 := make(chan string) // 默认通道,无缓冲// 可缓存通道允许在没有对应接收方的情况下,缓存限定数量的值// ch2 := make(chan string, 2) // 缓冲通道func main() { ch := make(chan string) // 创建channel go func(ch chan string) { // 启动协程异步执行任务 for i := 0; i < 3; i++ { fmt.Println(i) } ch "done" }(ch) // 阻塞等待任务完成 fmt.Println("task done")}
2,超时处理
package mainimport ( "fmt" "time")func main() { ch1 := make(chan string) // 创建channel1 ch2 := make(chan string) // 创建channel12 go func(ch chan string) { // 启动协程1 time.Sleep(time.Second * 1) ch "one" }(ch1) go func(ch chan string) { // 启动协程2 time.Sleep(time.Second * 2) ch "two" }(ch2) timeout := false for !timeout { select { case msg := fmt.Printf("received msg: %s\n", msg) case msg := fmt.Printf("received msg: %s\n", msg) case 5): fmt.Printf("timeout and exit\n") timeout = true } }}
输出如下
3,简单的crontab定时操作
package mainimport ( "fmt" "time")func main() { timer1 := time.NewTimer(time.Second * 3) // 创建一次性定时器,3秒后到期 go func() { // 使用定时器的通道进行阻塞 fmt.Println("start doing something") }() timer2 := time.NewTicker(time.Second * 2) // 创建周期性定时器,每隔2s执行一次 go func() { for t := range timer2.C { // 遍历定时器的通道 fmt.Println("Tick at", t) } }() 10) }
输出如下
我们来看下time.NewTimer,time.NewTicker的源码
都用到了通道,区别在于runtimeTimer{}结构体 period字段上,详细源码请大家自行研究。
4,多协程工作池
实现一个类似python multiprocessing.Pool的功能以下为python相关代码# 使用多线程池from multiprocessing.dummy import Poolimport timedef add(item): # sleep 1ms time.sleep(0.001) return item[0] + item[1]if __name__ == "__main__": st = time.time() * 1000 items = [] for i in range(100000): items.append((i, i)) p = Pool(100) # 创建线程池 results = p.map(add, items) # 并发执行add函数 et = time.time() * 1000 # 打印程序执行到此处的时间耗时 print("cost: %s ms" % (et - st)) for res in results: print(res)
以下为go 实现的同等功能代码,这里输入列表,执行函数没有支持泛型,使用了固定的类型,函数(这点后续再优化,主要是使用channel实现并发工作池)
package mainimport ( "errors" "fmt" "sort" "time")// 定义函数类型type Func func(int, int) int// 输入参数结构体type In struct { id int //序号 a int b int}// 输出结构体type Out struct { id int //序号 res int}// 结果列表,实现Len,Less, Swap函数,用于此结构体列表排序type Outs []Outfunc (self Outs) Len() int { return len(self)}func (self Outs) Less(i, j int) bool { return self[i].id < self[j].id}func (self Outs) Swap(i, j int) { self[i], self[j] = self[j], self[i]}// 协程池结构体type Pool struct { cIn chan In cOut chan Out goNum int handle Func}// 创建协程池函数func NewPool(num int) Pool { pool := Pool{} pool.goNum = num return pool}/*map函数,并发执行handle函数,结果是按照输入顺序排序的 */func (self Pool) Map(handle Func, items []In) (Outs, error) { if len(items) == 0 { return nil, errors.New("items lengths is 0") } self.cIn = make(chan In, len(items)) //输入通道 self.cOut = make(chan Out, len(items)) //输出通道 self.handle = handle // 启动工作协程池 for i := 0; i < self.goNum; i++ { go self.Worker(i) } // 输入列表发送到输入通道,触发协程工作 for _, item := range items { self.cIn fmt.Printf("send %v to cIn\n", item) } close(self.cIn) finishNum := 0 var result Outs for { // finish个数与输入个数一样时,退出循环 if finishNum == len(items) { break } val, ok := // 获取输出结果 // fmt.Println(val.id, val.res) if ok { result = append(result, val) finishNum ++ } } // 对结果按照序号排序 sort.Sort(result) return result, nil}func (self Pool) Worker(thId int) { for cIn := range self.cIn { fmt.Printf("thId: %d get In %v\n", thId, cIn) res := self.handle(cIn.a, cIn.b) self.cOut }}func add(a, b int) int { // sleep 1ms time.Sleep(time.Millisecond) return a + b}func main() { startTime := time.Now() items := []In{} for i := 0; i < 100000; i++ { items = append(items, In{i, i, i}) } pool := NewPool(100) // 类似python的map函数 outs, err := pool.Map(add, items) // 打印程序执行到这里的时间耗时 fmt.Println("cost", time.Since(startTime)) if err != nil { fmt.Println("error:", err) } else { for _, out := range outs { fmt.Println(out.id, out.res) } }}
以上就是使用channel实现工作池的代码,可以看出python的代码是多么的简洁,也就20行代码。我们对比下两者的性能
10万个元素,100个工作线程对比结果如下
go性能是python的2倍,当然由于python GIL的原因,Python的多线程是伪多线程。我们将工作线程的个数提升到1000个,go代码时间耗时146ms,python5分钟之内没有跑出结果,同时我的笔记本风扇开始工作了。这就是go一个线程可以同时跑成千上万个协程的好处了,当然协程数量越多也不是性能越快,太多协程底层锁竞争就会成为瓶颈。
5,实现互斥锁可以用channel实现互斥锁,当然内建互斥锁效率更高
package mainimport ( "fmt" "sync")type Lock struct { c chan struct{}}func NewLock() Lock { return Lock{c: make(chan struct{}, 1)}}func (self Lock) Lock() { self.c struct{}{}}func (self Lock) UnLock() { self.}func main() { wg := sync.WaitGroup{} lock := NewLock() count := 0 for i := 0; i < 1000; i++ { wg.Add(1) go func() { lock.Lock() count += 1 lock.UnLock() wg.Done() }() } wg.Wait() fmt.Println(count)}
channel的高级用法今天就先讲到这