NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

聊一聊 Go 中 channel 的行为

  • 2019-11-29
  • 本文字数:4456 字

    阅读完需:约 15 分钟

聊一聊Go中channel的行为

提到 Go 语言,最为亮点的特性,也就是 goroutine 的协程了。协程中我们用的最多的无非是 channel,小编最近也在研究 Go 语言的一些特性。但是对于 channel 理解不是很深,所以特地整理了本篇文章来深入理解一下 channel 的行为,跟大家分享一下。

1 简介

当我第一次开始使用 Go 的 channel 的时候,我犯了一个错误,认为 channel 是一个数据结构。我将 channel 看作是在 goroutine 之间提供自动同步访问的队列。这种结构上的理解使我编写了许多糟糕而复杂的并发代码。


随着时间的推移,我逐渐了解到,最好的办法是忘掉 channel 的结构,关注它们的行为。所以提到 channel,我想到了一个概念:信号。一个通道允许一个 goroutine 向另一个 goroutine 发出特定事件的信号。信号是应该使用 channel 做的一切的核心。将 channel 看作一种信号机制,可以让你编写具有明确定义和更精确的行为的代码。


要理解信号是如何工作的,我们必须理解它的三个属性:


  • 交付保证

  • 状态

  • 有数据或无数据


这三个属性共同构成了围绕信号的设计理念。 在讨论这些属性之后,我们将提供一些代码示例,这些示例演示如何使用这些属性进行信号传递。

2 交付保证

交付保证是基于一个问题:“我是否需要保证由特定的 goroutine 发送的信号已经被收到了?”


换句话说,我们可以给出下面这个示例:


01 go func() {02     p := <-ch // 接收03 }()0405 ch <- "paper" // 发送
复制代码


执行发送的 goroutine 是否需要保证,通过第 5 行被发送的一份报告(paper),在继续执行之前,被第 2 行要接收的 goroutine 接收到了?


根据这个问题的答案,你会知道使用两种 channel 中的哪一种:无缓冲或缓冲。每种 channel 在交付保证时提供不同的行为。



保证是很重要的。比如,如果你没有生活保证时,你不会紧张吗?在编写并发软件时,对是否需要保证有一个重要的理解是至关重要的。随着我们的继续,你将学会如何做出决定。

3 状态

channel 的行为直接受其当前状态的影响。channel 的状态可以为 nil,open 或 closed。


以下示例将介绍,如何在这三个状态中声明或设置一个 channel。


// ** nil channel
// 如果声明为零值的话,将会是nil状态var ch chan string
// 显式的赋值为nil,设置为nil状态ch = nil
// ** open channel
// 使用内部函数make创建的channel,为open状态ch := make(chan string) // ** closed channel
// 使用close函数的,为closed状态close(ch)
复制代码


状态决定了发送和接收操作的行为。


信号通过一个 channel 发送和接收。不可以称为读/写,因为 channel 不执行输入/输出。



当一个 channel 为 nil 状态时,channel 上的任何发送或接收都将被阻塞。当为 open 状态时,信号可以被发送和接收。如果被置为 closed 状态的话,信号不能再被发送,但仍有可能接收到信号。

4 有数据或无数据

需要考虑的最后一个信号属性是,信号是否带有数据。


通过在 channel 上执行发送带有数据的信号。


01 ch <- "paper"
复制代码


当你用数据发出信号时,通常是因为:


  • goroutine 被要求开始一项新任务。

  • goroutine 报告了一个结果。


通过关闭一个 channel 来发送没有数据的信号。


01 close(ch)
复制代码


当发送没有数据信号的时候,通常是因为:


  • goroutine 被告知要停止他们正在做的事情。

  • goroutine 报告说已经完成,没有结果。

  • goroutine 报告说它已经完成了处理,并且关闭。


没有数据的信号传递的一个好处是,一个单一的 goroutine 可以同时发出很多的信号。而在 goroutines 之间,用数据发送信号通常是一对一之间的交换。


有数据信号


当要使用数据进行信号传输时,您可以根据需要的担保类型选择三种 channel 配置选项。



这三个 channel 选项是无缓冲,缓冲>1 或缓冲=1。


  • 有保证

  • 因为信号的接收在信号发送完成之前就发生了。

  • 一个没有缓冲的通道可以保证发送的信号已经收到。

  • 无保证

  • 因为信号的发送是在信号接收完成之前发生的。

  • 一个大小>1 的缓冲通道不能保证发送的信号已经收到。

  • 延迟保证

  • 因为第一个信号的接收,在第二个信号发送完成之前就发生了。

  • 一个大小=1 的缓冲通道为您提供了一个延迟的保证。它可以保证发送的前一个信号已经收到。


缓冲区的大小绝不是一个随机数,它必须是为一些定义好的约束而计算出来的。在计算中没有无穷远,所有的东西都必须有一个明确定义的约束,无论是时间还是空间。


无数据信号


没有数据的信号,主要是为取消而预留的。它允许一个 goroutine 发出信号,让另一个 goroutine 取消他们正在做的事情,然后继续前进。取消可以使用非缓冲和缓冲 channel 来实现,但是在没有数据发送的情况下使用缓冲 channel 会更好。



内置函数 close 用于在没有数据的情况下发出信号。正如状态一节中介绍的,你仍然可以在一个关闭的通道接收到信号。事实上,在一个关闭的 channel 上的任何接收都不会阻塞,接收操作总是返回。


在大多数情况下,您希望使用标准库 context 包来实现无数据的信号传递。context 包使用一个没有缓冲的 channel 来进行信号传递,而内置函数 close 发送没有数据的信号。


如果选择使用自己的通道进行取消,而不是 context 包,那么你的通道应该是 chan struct{} 类型的。这是一种零空间的惯用方式,用来表示一个仅用于信号传输的 channel。


场景


有了这些属性,进一步了解它们在实践中工作的最佳方式就是运行一系列的代码场景。


有数据信号 - 保证 - 无缓冲的 channel


当你需要知道发送的信号已经收到时,就会有两种情况出现。一种是等待任务,另一种是等待结果。


场景 1 - 等待任务


设想你是一名经理,并雇佣了一名新员工。在这个场景中,你希望你的新员工执行一个任务,但是他们需要等待,直到你准备好。这是因为你需要在他们开始之前给他们一份报告(paper)。


01 func waitForTask() {02     ch := make(chan string)0304     go func() {05         p := <-ch0607         // 员工执行工作0809         // 员工可以自由地去做10     }()1112     time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)1314     ch <- "paper"15 }
复制代码


在 02 行中,无缓冲的 channel 被创建,string 类型的数据将被发送到信号中。然后在 04 行,一名员工被雇佣,并被告知在 05 行前等待你的信号,然后再做他们的工作。第 05 行是 channel 接收,导致员工在等待你发送的文件时阻塞。一旦员工收到了这份报告,员工就完成了工作,然后就可以自由地离开了。


你作为经理正在和你的新员工一起工作。因此,当你在第 04 行雇佣了员工后,你会发现自己(在第 12 行)做了你需要做的事情来解阻塞并且通知员工。值得注意的是,不知道要花费多长的时间来准备这份报告(paper)。


最终你准备好给员工发信号了。在第 14 行,你执行一个带有数据的信号,数据就是那份报告(paper)。由于使用了一个没有缓冲的 channel,所以当你的发送操作完成后,你就得到了该雇员已经收到该文件的保证。接收发生在发送之前。


场景 2 - 等待结果


在接下来的场景中,事情发生了反转。这一次,你希望你的新员工在被雇佣的时候立即执行一项任务,你需要等待他们工作的结果。你需要等待,因为在你可以继续之前,你需要他们的报告(paper)。


01 func waitForResult() {02     ch := make(chan string)0304     go func() {05         time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)0607         ch <- "paper"0809         // 员工已经完成了,并且可以自由地离开10     }()1112     p := <-ch13 }
复制代码


在第 02 行中,创建了一个没有缓冲的 channel,该 channel 的属性是 string 型数据将被发送到信号。然后在第 04 行,一名雇员被雇佣,并立即被投入工作。当你在第 04 行雇佣了这名员工后,你会发现自己排在第 12 行,等待着这份报告。


一旦工作由第 05 行中的员工完成,他们就会在第 07 行通过有数据的 channel 发送结果给你。由于这是一个没有缓冲的通道,所以接收在发送之前就发生了,并且保证你已经收到了结果。一旦员工有了这样的保证,他们就可以自由地工作了。在这种情况下,你不知道他们要花多长时间才能完成这项任务。


成本/效益


一个没有缓冲的通道可以保证接收到的信号被接收。这很好,但没有什么是免费的。这种担保的成本是未知的延迟。在等待任务场景的过程中,员工不知道要花多长时间才能发送那份报告。在等待结果的情况下,你不知道需要多长时间才能让员工发送结果。在这两种情况下,这种未知的延迟是我们必须要面对的,因为需要保证。如果没有这种保证行为,逻辑是行不通的。


以下场景请大家结合以上内容,具体分析查看。


有数据信号 - 无保证 - 缓冲的 channel > 1


01 func fanOut() {02     emps := 2003     ch := make(chan string, emps)0405     for e := 0; e < emps; e++ {06         go func() {07             time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)08             ch <- "paper"09         }()10     }1112     for emps > 0 {13         p := <-ch14         fmt.Println(p)15         emps--16     }17 }
复制代码


01 func selectDrop() {02     const cap = 503     ch := make(chan string, cap)0405     go func() {06         for p := range ch {07             fmt.Println("employee : received :", p)08         }09     }()1011     const work = 2012     for w := 0; w < work; w++ {13         select {14             case ch <- "paper":15                 fmt.Println("manager : send ack")16             default:17                 fmt.Println("manager : drop")18         }19     }2021     close(ch)22 }
复制代码


有数据信号 - 延迟保证 - 缓冲 channel 1


01 func waitForTasks() {02     ch := make(chan string, 1)0304     go func() {05         for p := range ch {06             fmt.Println("employee : working :", p)07         }08     }()0910     const work = 1011     for w := 0; w < work; w++ {12         ch <- "paper"13     }1415     close(ch)16 }
复制代码


无数据信号 - 上下文(Context)


01 func withTimeout() {02     duration := 50 * time.Millisecond0304     ctx, cancel := context.WithTimeout(context.Background(), duration)05     defer cancel()0607     ch := make(chan string, 1)0809     go func() {10         time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)11         ch <- "paper"12     }()1314     select {15     case p := <-ch:16         fmt.Println("work complete", p)1718     case <-ctx.Done():19         fmt.Println("moving on")20     }21 }
复制代码

总结

在使用 channel(或并发)时,关于保证、状态和发送的信号的属性非常重要。它们将帮助指导你实现你正在编写的并发程序和算法所需的最佳行为。它们将帮助你找到 bug,并找出潜在的糟糕代码。


在这篇文章中,我们分享了一些示例程序,它们展示了在不同场景中信号的属性是如何工作的。每个规则都有例外,但是这些模式是开始的良好基础。


本文转载自公众号 360 云计算(ID:hulktalk)。


原文链接:


https://mp.weixin.qq.com/s/qDFgMzF1onowF33_pczP-Q


2019-11-29 14:471097

评论 1 条评论

发布
用户头像
写的很好!受益匪浅
2019-12-27 21:40
回复
没有更多了
发现更多内容

[架构实战营一期] 模块四作业

trymorewang

架构实战营

架构实战营 - 毕业总结

༺NPE༻

Android开发:获取手机当前连接的WiFi名称

三掌柜

8月日更 8月

Python代码阅读(第8篇):列表元素逻辑判断

Felix

Python 编程 Code Programing 阅读代码

BERT模型为什么这么强?

博文视点Broadview

探究Presto SQL引擎(1)-巧用Antlr

vivo互联网技术

sql 编译器 presto antlr4

Spring梳理

十二万伏特皮卡丘

手撸二叉树之最小深度

HelloWorld杰少

数据结构与算法 8月日更

生产环境全链路压测平台Takin

TakinTalks稳定性社区

开源 高可用 全链路压测 性能压测

《社会心理学》--从众和服从

箭上有毒

8月日更

【Vue2.x 源码学习】第三十一篇 - diff算法-比对优化(下)

Brave

源码 vue2 8月日更

淘宝杨宽:淘宝直播低延迟架构演进和实践丨ECUG Meetup 回顾

七牛云

音视频 淘宝 ECUG 七牛云

RocketMQ之Store模块

邱学喆

commitlog consumequeue index ha flush

毕业设计:设计电商秒杀系统

贯通

#架构实战营

Druid 加载 Kafka 流数据 Supervisor 配置

HoneyMoose

十大排序算法--基数排序

Ayue、

排序算法 8月日更

Prometheus 监控SpringCloud微服务

Rubble

Prometheus SpringCloud 8月日更

SpringBoot 数据库操作(集成MyBatis)

xcbeyond

mybatis SpringCloud 8月日更

数据产品经理是做什么的?

蒋川

杜吉普:数字化运营广告营销之道

杜吉普

机器学习 互联网广告 营销数字化

【Flutter 专题】71 图解基本隐式动画 Widget

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 8月日更

docker入门:单机elasticsearch安装记录,保证无坑

小鲍侃java

8月日更

oeasy教您玩转vim - 12 - # 词头词尾

o

百度大脑DuMix AR携明星数字员工首次亮相China Joy,智能虚拟形象系统全面升级

百度大脑

人工智能 ChinaJoy 虚拟偶像 数字人

同为电动小车热门,欧拉白猫比拼零跑T03,谁更胜一筹?

科技热闻

Apache APISIX 在移动云的应用

API7.ai 技术团队

开源 网关 APISIX

书单 | 振聋发聩,撼世经典!总有那么一些书经得住时间的考验

博文视点Broadview

netty系列之:基于流的数据传输

程序那些事

Java Netty nio 程序那些事

“互联网+”大赛之AI创新应用赛题攻略:大胆脑洞,共绘智慧生活蓝图

华为云开发者联盟

AI HarmonyOS 大赛 互联网+ HMS Core

索信达控股:打造新一代中小银行智能营销平台体系的道与术

索信达控股

WeLink的杀手锏和远程办公软件的另一面

华为云开发者联盟

远程办公 华为云 welink 视频会议 数字化办公

聊一聊Go中channel的行为_文化 & 方法_PlatformDev_InfoQ精选文章