AICon 北京站 Keynote 亮点揭秘,想了解 Agent 智能体来就对了! 了解详情
写点什么

聊一聊 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:471515

评论 1 条评论

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

直播预告 | 企业如何轻松完成数据治理?火山引擎DataLeap给你一份实战攻略!

字节跳动数据平台

数据库 大数据 数据治理 数据实践

推荐系统[二]:召回算法超详细讲解[召回模型演化过程、召回模型主流常见算法(DeepMF_TDM_Airbnb Embedding_Item2vec等)、召回路径简介、多路召回融合]

汀丶人工智能

自然语言处理 深度学习 推荐系统 搜索算法 召回算法

项目终于用上了低代码,才知道为什么真香了!

引迈信息

项目管理 低代码

秒懂算法 | 莫队算法

TiAmo

算法 暴力猜解

MySql基础-笔记6 -排序、分组、连接的使用、NULL值处理

MySQL 数据库

软件测试 | 霍格沃兹线下班开课啦!

测吧(北京)科技有限公司

测试

使用 NGINX 在 Kubernetes 中对 TCP 和 UDP 流量进行负载均衡

NGINX开源社区

nginx Kuber udp tpc 企业号 2 月 PK 榜

OKR之剑·总结篇01:如何开好一场OKR复盘会

vivo互联网技术

团队管理 OKR

MySql基础-笔记11-临时表、复制表、元数据、序列使用

MySQL 数据库

飞桨全量支持业内AI科学计算工具——DeepXDE!

飞桨PaddlePaddle

人工智能 深度学习 开源

本周 2 场直播预告!Intel 高级工程师带你探索开源机密计算社区 CCZoo | 第 65 期

OpenAnolis小助手

云计算 运维 直播 intel 龙蜥大讲堂

Unittest接口和UI自动化测试框架中的发送邮件服务如何使用?

Python 自动化测试 unittest 邮件服务

MASA MAUI Plugin (八)Android相册多选照片(Intent 方式)

MASA技术团队

.net blazor MAUI MASA Blazor

风险洞察之事件总线的探索与演进

京东科技开发者

京东云 事件总线 京东技术 数据管道 风险洞察

微服务拆分治理最佳实践

京东科技开发者

数据库 微服务 京东云 京东技术 安全接口

架构实战营模块6 拆分电商系统为微服务

西山薄凉

「架构实战营」

自研的内存分析利器开源了!Android Bitmap Monitor 助你定位不合理的图片使用

拭心

android 性能优化 BitMap 内存优化

MySql基础-笔记5 -WHERE 、UPDATE、DELETE、LIKE、UNION使用

MySQL 数据库

业务架构那点事(1)业务架构师就是在“盖房子”

涛哥 数字产品和业务架构

企业架构 业务架构

单元测试利器——手把手教你使用Mockito

京东科技开发者

单元测试 Mockito 京东云 安全测试 京东技术

MySQL审计插件-MariaDB Audit Plugin

GreatSQL

:MySQL 数据库 maria greatsql greatsql社区

MySql基础-笔记10-索引

MySQL 数据库

中科协发布2022“科创中国”开源创新榜 OceanBase开源社区入选

OceanBase 数据库

数据库 oceanbase

乌卡时代的云成本管理:从0到1了解FinOps

SEAL安全

云服务 FinOps 企业号 2 月 PK 榜

搜索EE场景排序链路升级

京东科技开发者

模型 搜索 EE 企业号 2 月 PK 榜 Explore & Exploit

定了!Python3.7,今年停止更新!

程序员晚枫

Python 软件 下载 版本

MySql基础-笔记4 -数据表创建、删除和数据插入、查询等操作

MySQL 数据库 删除 创建

长安链入选“2022科创中国”开源创新榜

科技热闻

aspnetcore 原生 DI 实现基于 key 的服务获取

newbe36524

C# Docker Kubernetes

MySql基础-笔记9 -ALTER命令

MySQL 数据库

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