0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

使用golang channel的诸多特性和技巧

马哥Linux运维 来源:GoDaddy. 作者:GoDaddy. 2021-09-06 15:14 次阅读

本文介绍了使用 golang channel 的诸多特性和技巧,已经熟悉了 go 语言特性的小伙伴也可以看看,很有启发。 不同于传统的多线程并发模型使用共享内存来实现线程间通信的方式,golang 的哲学是通过 channel 进行协程 (goroutine) 之间的通信来实现数据共享:

Do not communicate by sharing memory; instead, share memory by communicating.

这种方式的优点是通过提供原子的通信原语,避免了竞态情形 (race condition) 下复杂的锁机制。channel 可以看成一个 FIFO 队列,对 FIFO 队列的读写都是原子的操作,不需要加锁。对 channel 的操作行为结果总结如下:

操作 nil channel closed channel not-closed non-nil channel
close panic panic 成功 close
写ch <- 一直阻塞 panic 阻塞或成功写入数据
读<- ch 一直阻塞 读取对应类型零值 阻塞或成功读取数据

读取一个已关闭的 channel 时,总是能读取到对应类型的零值,为了和读取非空未关闭 channel 的行为区别,可以使用两个接收值:

//okisfalsewhenchisclosed v,ok:=<-chgolang 中大部分类型都是值类型(只有 slice / channel / map 是引用类型),读/写类型是值类型的 channel 时,如果元素 size 比较大时,应该使用指针代替,避免频繁的内存拷贝开销。

内部实现

如图所示,在 channel 的内部实现中(具体定义在$GOROOT/src/runtime/chan.go里),维护了 3 个队列:

读等待协程队列 recvq,维护了阻塞在读此 channel 的协程列表

写等待协程队列 sendq,维护了阻塞在写此 channel 的协程列表

缓冲数据队列 buf,用环形队列实现,不带缓冲的 channel 此队列 size 则为 0

img 当协程尝试从未关闭的 channel 中读取数据时,内部的操作如下:

当 buf 非空时,此时 recvq 必为空,buf 弹出一个元素给读协程,读协程获得数据后继续执行,此时若 sendq 非空,则从 sendq 中弹出一个写协程转入 running 状态,待写数据入队列 buf ,此时读取操作<- ch 未阻塞;

当 buf 为空但 sendq 非空时(不带缓冲的 channel),则从 sendq 中弹出一个写协程转入 running 状态,待写数据直接传递给读协程,读协程继续执行,此时读取操作<- ch 未阻塞;

当 buf 为空并且 sendq 也为空时,读协程入队列 recvq 并转入 blocking 状态,当后续有其他协程往 channel 写数据时,读协程才会重新转入 running 状态,此时读取操作<- ch 阻塞。

类似的,当协程尝试往未关闭的 channel 中写入数据时,内部的操作如下:

当队列 recvq 非空时,此时队列 buf 必为空,从 recvq 弹出一个读协程接收待写数据,此读协程此时结束阻塞并转入 running 状态,写协程继续执行,此时写入操作ch <- 未阻塞;

当队列 recvq 为空但 buf 未满时,此时 sendq 必为空,写协程的待写数据入 buf 然后继续执行,此时写入操作ch <- 未阻塞;

当队列 recvq 为空并且 buf 为满时,此时写协程入队列 sendq 并转入 blokcing 状态,当后续有其他协程从 channel 中读数据时,写协程才会重新转入 running 状态,此时写入操作ch <- 阻塞。

当关闭 non-nil channel 时,内部的操作如下:

当队列 recvq 非空时,此时 buf 必为空,recvq 中的所有协程都将收到对应类型的零值然后结束阻塞状态;

当队列 sendq 非空时,此时 buf 必为满,sendq 中的所有协程都会产生 panic ,在 buf 中数据仍然会保留直到被其他协程读取。

使用场景

除了常规的用来在协程之间传递数据外,本节列出了一些特殊的使用 channel 的场景。

futures / promises

golang 虽然没有直接提供 futrue / promise 模型的操作原语,但通过 goroutine 和 channel 可以实现类似的功能:

packagemain import( "io/ioutil" "log" "net/http" ) //RequestFuture,httprequestpromise. funcRequestFuture(urlstring)<-chan []byte {     c := make(chan []byte, 1)     go func() {         var body []byte         defer func() {             c <- body         }()         res, err := http.Get(url)         if err != nil {             return         }         defer res.Body.Close()         body, _ = ioutil.ReadAll(res.Body)     }()     return c } func main() {     future := RequestFuture("https://api.github.com/users/octocat/orgs")     body := <-future     log.Printf("reponse length: %d", len(body)) }

条件变量 (condition variable)

类型于 POSIX 接口中线程通知其他线程某个事件发生的条件变量,channel 的特性也可以用来当成协程之间同步的条件变量。因为 channel 只是用来通知,所以 channel 中具体的数据类型和值并不重要,这种场景一般用strct {}作为 channel 的类型。

一对一通知

类似pthread_cond_signal()的功能,用来在一个协程中通知另个某一个协程事件发生:

packagemain import( "fmt" "time" ) funcmain(){ ch:=make(chanstruct{}) nums:=make([]int,100) gofunc(){ time.Sleep(time.Second) fori:=0;i< len(nums); i++ {             nums[i] = i         }         // send a finish signal         ch <- struct{}{}     }()     // wait for finish signal     <-ch     fmt.Println(nums) }

广播通知

类似pthread_cond_broadcast()的功能。利用从已关闭的 channel 读取数据时总是非阻塞的特性,可以实现在一个协程中向其他多个协程广播某个事件发生的通知:

packagemain import( "fmt" "time" ) funcmain(){ N:=10 exit:=make(chanstruct{}) done:=make(chanstruct{},N) //startNworkergoroutines fori:=0;i< N; i++ {         go func(n int) {             for {                 select {                 // wait for exit signal                 case <-exit:                     fmt.Printf("worker goroutine #%d exit ", n)                     done <- struct{}{}                     return                 case <-time.After(time.Second):                     fmt.Printf("worker goroutine #%d is working... ", n)                 }             }         }(i)     }     time.Sleep(3 * time.Second)     // broadcast exit signal     close(exit)     // wait for all worker goroutines exit     for i := 0; i < N; i++ {         <-done     }     fmt.Println("main goroutine exit") }

信号

channel 的读/写相当于信号量的 P / V 操作,下面的示例程序中 channel 相当于信号量:

packagemain import( "log" "math/rand" "time" ) typeSeatint typeBarchanSeat func(barBar)ServeConsumer(customerIdint){ log.Print("->consumer#",customerId,"entersthebar") seat:=<-bar // need a seat to drink     log.Print("consumer#", customerId, " drinks at seat#", seat)     time.Sleep(time.Second * time.Duration(2+rand.Intn(6)))     log.Print("<- consumer#", customerId, " frees seat#", seat)     bar <- seat // free the seat and leave the bar } func main() {     rand.Seed(time.Now().UnixNano())     bar24x7 := make(Bar, 10) // the bar has 10 seats     // Place seats in an bar.     for seatId := 0; seatId < cap(bar24x7); seatId++ {         bar24x7 <- Seat(seatId) // none of the sends will block     }     // a new consumer try to enter the bar for each second     for customerId := 0; ; customerId++ {         time.Sleep(time.Second)         go bar24x7.ServeConsumer(customerId)     } }

互斥量

互斥量相当于二元信号里,所以 cap 为 1 的 channel 可以当成互斥量使用:

packagemain import"fmt" funcmain(){ mutex:=make(chanstruct{},1)//thecapacitymustbeone counter:=0 increase:=func(){ mutex<- struct{}{} // lock         counter++         <-mutex // unlock     }     increase1000 := func(done chan<- struct{}) {         for i := 0; i < 1000; i++ {             increase()         }         done <- struct{}{}     }     done := make(chan struct{})     go increase1000(done)     <-done; <-done     fmt.Println(counter) // 2000 }

关闭 channel

关闭不再需要使用的 channel 并不是必须的。跟其他资源比如打开的文件、socket 连接不一样,这类资源使用完后不关闭后会造成句柄泄露,channel 使用完后不关闭也没有关系,channel 没有被任何协程用到后最终会被 GC 回收。关闭 channel 一般是用来通知其他协程某个任务已经完成了。golang 也没有直接提供判断 channel 是否已经关闭的接口,虽然可以用其他不太优雅的方式自己实现一个:

funcisClosed(chchanint)bool{ select{ case<-ch:         return true     default:     }     return false }不过实现一个这样的接口也没什么必要。因为就算通过 isClosed() 得到当前 channel 当前还未关闭,如果试图往 channel 里写数据,仍然可能会发生 panic ,因为在调用 isClosed() 后,其他协程可能已经把 channel 关闭了。关闭 channel 时应该注意以下准则:

不要在读取端关闭 channel ,因为写入端无法知道 channel 是否已经关闭,往已关闭的 channel 写数据会 panic ;

有多个写入端时,不要再写入端关闭 channle ,因为其他写入端无法知道 channel 是否已经关闭,关闭已经关闭的 channel 会发生 panic ;

如果只有一个写入端,可以在这个写入端放心关闭 channel 。

关闭 channel 粗暴一点的做法是随意关闭,如果产生了 panic 就用 recover 避免进程挂掉。稍好一点的方案是使用标准库的sync包来做关闭 channel 时的协程同步,不过使用起来也稍微复杂些。下面介绍一种优雅些的做法。

一写多读

这种场景下这个唯一的写入端可以关闭 channel 用来通知读取端所有数据都已经写入完成了。读取端只需要用for range把 channel 中数据遍历完就可以了,当 channel 关闭时,for range仍然会将 channel 缓冲中的数据全部遍历完然后再退出循环:

packagemain import( "fmt" "sync" ) funcmain(){ wg:=&sync.WaitGroup{} ch:=make(chanint,100) send:=func(){ fori:=0;i< 100; i++ {             ch <- i         }         // signal sending finish         close(ch)     }     recv := func(id int) {         defer wg.Done()         for i := range ch {             fmt.Printf("receiver #%d get %d ", id, i)         }         fmt.Printf("receiver #%d exit ", id)     }     wg.Add(3)     go recv(0)     go recv(1)     go recv(2)     send()     wg.Wait() }

多写一读

这种场景下虽然可以用sync.Once来解决多个写入端重复关闭 channel 的问题,但更优雅的办法设置一个额外的 channel ,由读取端通过关闭来通知写入端任务完成不要再继续再写入数据了:

packagemain import( "fmt" "sync" ) funcmain(){ wg:=&sync.WaitGroup{} ch:=make(chanint,100) done:=make(chanstruct{}) send:=func(idint){ deferwg.Done() fori:=0;;i++{ select{ case<-done:                 // get exit signal                 fmt.Printf("sender #%d exit ", id)                 return             case ch <- id*1000 + i:             }         }     }     recv := func() {         count := 0         for i := range ch {             fmt.Printf("receiver get %d ", i)             count++             if count >=1000{ //signalrecvingfinish close(done) return } } } wg.Add(3) gosend(0) gosend(1) gosend(2) recv() wg.Wait() }

多写多读

这种场景稍微复杂,和上面的例子一样,也需要设置一个额外 channel 用来通知多个写入端和读取端。另外需要起一个额外的协程来通过关闭这个 channel 来广播通知:

packagemain import( "fmt" "sync" "time" ) funcmain(){ wg:=&sync.WaitGroup{} ch:=make(chanint,100) done:=make(chanstruct{}) send:=func(idint){ deferwg.Done() fori:=0;;i++{ select{ case<-done:                 // get exit signal                 fmt.Printf("sender #%d exit ", id)                 return             case ch <- id*1000 + i:             }         }     }     recv := func(id int) {         defer wg.Done()         for {             select {             case <-done:                 // get exit signal                 fmt.Printf("receiver #%d exit ", id)                 return             case i := <-ch:                 fmt.Printf("receiver #%d get %d ", id, i)                 time.Sleep(time.Millisecond)             }         }     }     wg.Add(6)     go send(0)     go send(1)     go send(2)     go recv(0)     go recv(1)     go recv(2)     time.Sleep(time.Second)     // signal finish     close(done)     // wait all sender and receiver exit     wg.Wait() }

总结

channle 作为 golang 最重要的特性,用起来还是比较爽的。传统的 C 里要实现类型的功能的话,一般需要用到 socket 或者 FIFO 来实现,另外还要考虑数据包的完整性与并发冲突的问题,channel 则屏蔽了这些底层细节,使用者只需要考虑读写就可以了。channel 是引用类型,了解一下 channel 底层的机制对更好的使用 channel 还是很用必要的。

虽然操作原语简单,但涉及到阻塞的问题,使用不当可能会造成死锁或者无限制的协程创建最终导致进程挂掉。 channel 除在可以用来在协程之间通信外,其阻塞和唤醒协程的特性也可以用作协程之间的同步机制,文中也用示例简单介绍了这种场景下的用法。

关闭 channel 并不是必须的,只要没有协程没用引用 channel ,最终会被 GC 清理。所以使用的时候要特别注意,不要让协程阻塞在 channel 上,这种情况很难检测到,而且会造成 channel 和阻塞在 channel 的协程占有的资源无法被 GC 清理最终导致内存泄露。

channle 方便 golang 程序使用 CSP 的编程范形,但是 golang 是一种多范形的编程语言,golang 也支持传统的通过共享内存来通信的编程方式。终极的原则是根据场景选择合适的编程范型,不要因为 channel 好用而滥用 CSP 。

转自:http://litang.me/post/golang-channel/

编辑:jq

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 阻塞
    +关注

    关注

    0

    文章

    24

    浏览量

    8101
  • root
    +关注

    关注

    1

    文章

    86

    浏览量

    21392
  • go语言
    +关注

    关注

    1

    文章

    158

    浏览量

    9048

原文标题:golang channel 使用总结

文章出处:【微信号:magedu-Linux,微信公众号:马哥Linux运维】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    ADS1259 AD转换根据输出编码channel data,怎么才能知道输出电压?

    ADS1259 AD转换根据输出编码channel data,怎么才能知道输出电压呢?
    发表于 12-12 07:36

    ADS8664的Auxiliary Channel作为输入的话,需要怎么处理?

    ADS8664的Auxiliary Channel作为输入的话,需要怎么处理?看手册这个通过与其他正常的通道不一样,没有过压保护之类的,采样原理也与其他通道不一致。总结一下问题: 1. 该通道是否不像其他通道一样有过压保护; 2. 该通道RC滤波参数应该怎么选择,是否必须要加
    发表于 11-22 06:31

    Golang配置代理方法

    由于一些客观原因的存在,我们开发 Golang 项目的过程总会碰到无法下载某些依赖包的问题。这不是一个小问题,因为你的工作会被打断,即便你使用各种神通解决了问题,很可能这时你的线程已经切换到其他的事情上了(痛恨思路被打断!)。所以最好是一开始我们就重视这个问题,并一劳永逸的解决它。
    的头像 发表于 11-11 11:17 227次阅读
    <b class='flag-5'>Golang</b>配置代理方法

    N沟道场效应管和P沟道场效应管有什么区别

    , P-Channel FET)是场效应管(Field Effect Transistor, FET)的两种基本类型,它们在导电机制、极性、驱动电压、导通电阻、噪声特性、温度特性以及应用领域等方面存在显著差异。以下是对这两种场效
    的头像 发表于 09-23 16:38 1750次阅读

    【米尔NXP i.MX 93开发板试用评测】4、使用golang搭建Modbus 服务器

    负责处理来自客户端(通常称为Modbus客户端或从站)的请求,并根据请求提供相应的数据或执行操作。 快速开发modbus服务器 可以使用golang快速部署一个modbus服务器。我们先在开发板上安装
    发表于 09-21 22:51

    吉时利2450数字源表如何分析信号的频谱特性

    的重要性与挑战 信号的频谱特性包含了丰富的信息,如信号的频率成分、幅度分布、谐波含量等。了解信号的频谱特性可以帮助工程师诊断电子系统中的问题,优化威廉希尔官方网站 设计,提高系统性能。然而,分析信号的频谱特性并非易事,面临着
    的头像 发表于 08-26 16:50 279次阅读

    9-CHANNEL RS-422/RS-485收发器数据表

    电子发烧友网站提供《9-CHANNEL RS-422/RS-485收发器数据表.pdf》资料免费下载
    发表于 07-09 09:15 0次下载
    9-<b class='flag-5'>CHANNEL</b> RS-422/RS-485收发器数据表

    stm32g431cbu6 DAC DAC_CHANNEL_1无法输出电压的原因?

    一个很简单的问题,我的 DAC DAC_CHANNEL_1无法输出电压。 1.首先我的ioc配置界面如下: 2.初始化代码如下: / USER CODE BEGIN Header
    发表于 05-20 07:27

    GOLANG接口三个特性介绍

    变量i的类型为int,变量j的类型为MyInt,变量i、j具有确定的类型,虽然i、j的潜在类型是一样的,但是在没有转换的情况下他们之间不能相互赋值。
    的头像 发表于 04-16 11:40 411次阅读

    Golang为何舍弃三元运算符

    golang中不存在?:运算符的原因是因为语言设计者已经预见到三元运算符经常被用来构建一些极其复杂的表达式。虽然使用if进行替代会让代码显得更长,但这毫无疑问可读性更强。
    的头像 发表于 04-03 15:13 700次阅读

    STM32CUBEIDE对芯片STM32G0B1RE配置ADC1_IN15时,Channel无法进行选定怎么解决?

    上图为配置界面,Channel无法进行选定,该怎么解决?感谢!
    发表于 03-28 07:14

    Multi-Channel PCIe QDMA&RDMA IP应用介绍

    基于PCI Express Integrated Block,Multi-Channel PCIe QDMA Subsystem实现了使用DMA地址队列的独立多通道、高性能Continous或
    发表于 02-22 14:34 1次下载

    【米尔-全志T113-i开发板试用】3、使用golang获取系统信息

    (duration.Hours()/24)) 程序的整体输出如下 由于golang可以跨平台开发,我们先在X86的电脑上开发,然后交叉编译放到米尔的全志T113-i开发板上运行就可以了。板子上运行的效果
    发表于 02-22 09:39

    TOM0 channel5中断异常的原因?

    正常情况下ADC中断和TOM0 channel5中断都是正常的,当出现异常时,ADC中断正常,TOM0 channel5中断有中断请求,但是不触发中断,ADC中断优先级高,当UDE里面将ADC中断
    发表于 01-25 06:33

    如何使用Golang连接MySQL

    首先我们来看如何使用Golang连接MySQL。
    的头像 发表于 01-08 09:42 3376次阅读
    如何使用<b class='flag-5'>Golang</b>连接MySQL