golang之channel学习总结

it2024-04-02  62

概述

本篇目的是对go中的channel做一个总结。 主要参考https://www.jianshu.com/p/76acce09da09

环境

$ uname -a Linux gl.com 5.4.50-amd64-desktop #74 SMP Mon Aug 24 20:15:37 CST 2020 x86_64 GNU/Linux $ go version go version go1.15.2 linux/amd64

channel的用途

主要用于goroutine之间通信

channel的基本操作和注意事项

在总结之前只记得有这么几条:

channel不能重复关闭, 否则会panic已经关闭的channel, 再读取会到零值…

关于close函数可阅读我之前的文章:golang中的close函数

总的来说, 关于channel有三种操作: 读(<-ch), 写(ch <- value), 关(close(ch)), 操作可能出现的情况总结如下:

channel类型 \ 操作读(<-ch)写(ch <- value)关(close(ch))nil(未make)阻塞阻塞panic: close of nil channel正常(已make且未close)成功或阻塞成功或阻塞成功已关闭读到零值panicpanic

所以对于nil channel在select…case中的情况, 相应的case分支会一直进不去, 如下面的ch:

func nil_channel_in_select() { var ch chan int ch2 := make(chan int) rand.NewSource(time.Now().Unix()) go func() { for { ch2 <- rand.Int() time.Sleep(time.Second) } }() for { // 对于ch的读写的case都不会进 select { case <-ch: fmt.Println("read from nil channel") case ch <- 1: fmt.Println("write to nil channel") case v := <-ch2: // 如果没这个case, 也会死锁 // fatal error: all goroutines are asleep - deadlock! fmt.Println("read value from ch2:", v) } } }

channel的常见用法

1. 使用for…range选取channel的值

使用for-range读取channel,当channel关闭时,for循环会自动退出,无需主动监测channel是否关闭,可以防止读取已经关闭的channel,造成读到数据为通道所存储的数据类型的零值。如:

func read_channel_by_for_range() { ch := make(chan int) go func() { for i := 0; i < 5; i++ { ch <- i * i } close(ch) // 应该在发送者处适时关闭channel }() for x := range ch { fmt.Println(x) } }

2. 使用_, ok判断channel是否关闭

读已关闭的channel会得到零值,如果不确定channel是否已经关闭,需要使用ok进行检测。ok的结果和含义:

true:读到数据,并且通道没有关闭。false:通道关闭,没数据读到。

如:

func judge_channel_closed_by_ok() { ch := make(chan int) go func() { for i := 0; i < 5; i++ { ch <- i * i } close(ch) // 应该在发送者处适时关闭channel }() for { x, ok := <-ch if !ok { // 检测到 channel已经关闭时, 退出循环 fmt.Println("channel closed") break } fmt.Println(x, ok) } }

3. 使用select处理多个channel(可以处理ctx.Done()做优雅退出)

select可以同时监控多个通道的情况,只处理未阻塞的case(如果有不止一个case ready, 则随机选择一个。当通道为nil时,对应的case永远为阻塞,无论读写。如:

func select_case() { ch1 := make(chan int) ch2 := make(chan int) timer := time.NewTicker(time.Second) ctx, cancel := context.WithCancel(context.Background()) go func() { for i := 0; i < 5; i++ { ch1 <- i * i time.Sleep(time.Second) } close(ch1) }() go func() { for i := 0; i < 5; i++ { ch2 <- i + i time.Sleep(time.Second * 2) } close(ch2) }() go func() { // 5秒后cancel time.Sleep(time.Second * 5) cancel() }() for { select { case _, ok := <-ch1: if !ok { fmt.Println("ch1 closed") } else { fmt.Println("read from ch1, now is:", time.Now()) } case _, ok := <-ch2: if !ok { fmt.Println("ch2 closed") } else { fmt.Println("read from ch2, now is:", time.Now()) } case <-ctx.Done(): // 发现cancel, 做一些善后工作, 然后退出 fmt.Println("ctx.Done()") return case <-timer.C: fmt.Println("read from timer, now is:", time.Now()) } } }

4. 使用channel的声明控制读写权限

channel是可以定义为只读/只写的, 如:

// 定义只读/只写channel // read_only := make(<-chan int) // read_only <- 1 // Invalid operation: read_only <- 1 (send to receive-only type <-chan int) // write_only := make(chan<- int) // <-write_only // Invalid operation: <-write_only (receive from send-only type chan<- int)

但是这样没啥实际意义, 数据只能一边进, 别人怎么用啊?

一般还是用于函数参数及返回值等,这样可以使得参数或者返回址的意义更明确, 如:

只读channel(这个在项目中用得多一些):

// 返回一个channel, 里边放的是n以内的的质数 // 显示返回的channel外界只需要读就行了, 因为结果我们已经在函数内计算好了 func readonly_channel(n int) <-chan int { ch := make(chan int) // 索引为相应的数, 值表示当前索引是否为质数 prime := make([]bool, n+1) for i := range prime { // 初始化都是质数 prime[i] = true } go func() { defer close(ch) for i := 2; i <= n; i++ { if prime[i] { // 如果是质数 for j := 2; j*i <= n; j++ { // 标记所有该质数的倍数均为合数 prime[j*i] = false } ch <- i } } }() return ch } // 使用: for x := range readonly_channel(50) { fmt.Println(x) }

只写channel(感觉这个没怎么用过):

signal.Notify(c chan<- os.Signal, sig ...os.Signal)是一个例子。

不太恰当的例子:

// 往给定的ch中写入n以内的奇数 func writeonly_channel(ch chan<- int, n int) { go func() { defer close(ch) for i := 1; i <= n; i++ { if i&1 == 1 { // n以内的所有奇数 ch <- i } } }() } // 使用 ch := make(chan int) writeonly_channel(ch, 10) for odd := range ch { fmt.Println(odd) }

5. 使用缓冲channel增强并发

channel是可以带缓冲的, 缓冲嘛, 一定程度上可以提高并发度。 写一个, 还能继续写,不用等读出去了再写。如:

// 生成n个数到channel中, 看看不同buffer_size下的区别 func gen_num(n int, buffer_size int) <-chan int { ch := make(chan int, buffer_size) go func() { defer close(ch) for i := 1; i <= n; i++ { ch <- i } }() return ch } func calc_time_for_gen_num() { for i := 0; i <= runtime.NumCPU(); i++ { start := time.Now() for range gen_num(1e7, i) { } fmt.Printf("buffer_size: %d, used time(ns): %d\n", i, time.Now().Sub(start).Nanoseconds()) } }

最后跑出来的结果:

buffer_size: 0, used time(ns): 2126284502 buffer_size: 1, used time(ns): 1706649198 buffer_size: 2, used time(ns): 1302575739 buffer_size: 3, used time(ns): 1087461655 buffer_size: 4, used time(ns): 1248243529 buffer_size: 5, used time(ns): 1175520107 buffer_size: 6, used time(ns): 1022376191 buffer_size: 7, used time(ns): 878169585 buffer_size: 8, used time(ns): 789670245

从结果可以看到, 缓冲越大, 在上面代码的情况下用时越少。

6. 为操作加上超时

func timed_op(timeout time.Duration) { noop_loop := func(n int) <-chan bool { ch := make(chan bool) go func() { time.Sleep(time.Second) close(ch) }() return ch } start := time.Now() select { case <-noop_loop(1e10): fmt.Printf("finished in %d ms\n", timeout/time.Millisecond) case <-time.After(timeout): // 在timeout时间内如果noop_loop还没处理完(未关闭ch) // 就会到这里来 // 然后就可以做一些超时处理 fmt.Println("timeout after:", timeout) } fmt.Println("used ", time.Now().Sub(start)) } // 调用 timed_op(time.Second * 2)

7. 使用close(ch)关闭所有下游协程(做优雅退出用)

其实context包中的cancel方法就是使用的close方法来使得调用方的<-ctx.Done()可以调用, 进而知道上层已经cancel(). 看代码:

func elegantly_exit() { ctx, cancel := context.WithCancel(context.Background()) // 用来通知上游, 表示业务已经处理完了 // 一般是一些善后工作,如: 将缓冲的消息尽快发出去 closed := make(chan struct{}) // 一些业务代码 go func(ctx context.Context) { for { select { case x := <-ctx.Done(): fmt.Println("ctx.Done():", x) close(closed) return default: } fmt.Println("handle business") time.Sleep(time.Second) } }(ctx) exitCh := make(chan os.Signal) signal.Notify(exitCh, syscall.SIGINT, syscall.SIGTERM) // 收到SITINT, SIGTERM信号后, 系统会将相应的信号写到exitCh中 // 否则会一直卡在这 sig := <-exitCh fmt.Println("received signal:", sig) // 通知下游goroutine cancel() // 表示下游goroutine已经做完善后工作了 <-closed }

8. 更多技巧后续补充

参考

https://www.jianshu.com/p/76acce09da09

(完)

最新回复(0)