当谈到并发时,许多编程语言都采用共享内存/状态模型。然而,Go 通过实现 Communicating Sequential Processes(CSP)而与众不同。在 CSP 中,程序由不共享状态的并行处理器组成;相反,他们使用 Channel 来沟通和同步他们的行动。因此,对于有兴趣采用 Go 的开发人员来说,理解 Channel 的工作原理变得至关重要。在本文中,我将使用地鼠经营他们想象中的咖啡馆的令人愉快的类比来说明 Channel ,因为我坚信人类是更好的视觉学习者。

场景

Partier, Candier, Stringer 三人正在经营一家咖啡馆。鉴于制作咖啡比接受订单需要更多时间,Partier 将协助接受顾客的订单,然后将这些订单传递到厨房,由 Candier 和 Stringer 准备咖啡。

无缓冲 Channels

最初,咖啡馆以最简单的方式运营:每当收到新订单时,Partier 都会将订单放入 Channel 中,并等到 Candier 或 Stringer 接受后才接受任何新订单。 Partier 和厨房之间的通信是通过使用 ch := make(chan Order) 创建的无缓冲 Channel 来实现的。当 Channel 中没有挂单时,即使 Stringer 和 Candier 都准备好接受新订单,它们也会保持空闲状态并等待新订单到达。

当收到新订单时,Partier 将其放入 Channel 中,使 Candier 或 Stringer 可以接受该订单。但是,在继续接受新订单之前,Partier 必须等待后厨的两位工作者(Candier, Stringer)的其中一个从 Channel 中检索并获取订单。

由于 Candier 和 Stringer 都可以接受新订单,因此他们中的任何一个都会立即接受新订单。但是,无法保证或预测收到订单的具体收件人。 Stringer 和 Candier 之间的选择是不确定的,它取决于调度和 Go 运行时的内部机制等因素。假设 Candier 收到了第一个订单。

Candier 处理完第一个订单后,又回到等待状态。如果没有新订单到达,Candier 和 Stringer 这两个工作人员将保持空闲状态,直到 Partier 将另一个订单放入通道中供他们处理。

当新订单到达时,Stringer 和 Candier 都可以处理它。即使 Candier 刚刚处理了上一个订单,接收新订单的具体工人仍然是不确定的。在这种情况下,假设 Candier 再次被分配了第二个订单。

新订单 order3 到达,Candier 当前正忙于处理 order2,她没有在队列中等待 order := <-ch,Stringer 成为唯一可以接收 order3 的工作人员。因此,他会得到它。

在 order3 发送到 Stringer 后,order4 立即到达。此时,Stringer 和 Candier 都已忙于处理各自的订单,没有人可以接收 order4。由于 Channel 没有缓冲,因此将 order4 放入其中会阻塞 Partier,直到 Stringer 或 Candier 可以接受 order4。这种情况值得特别注意,因为我经常看到人们对无缓冲通道(使用 make(chan order) 或 make(chan order, 0) 创建)和缓冲区大小为 1 的通道(使用 make(chan order, 1) 创建)感到困惑)。因此,他们错误地期望 ch <- order4 立即完成并接收 order5。如果您也是这么想的,我在 Go Playground 上创建了一个片段来帮助您纠正您的误解 https://go.dev/play/p/shRNiDDJYB4

带缓冲的 Channel

无缓冲通道可以工作,但是它限制了整体吞吐量。如果可以接受更多订单并在后端(厨房)按顺序处理它们,那就更好了。这可以通过缓冲通道来实现。现在,即使 Stringer 和 Candier 忙于处理订单,Partier 仍然可以在通道中留下新订单,并在通道未满的情况下继续接受其他订单,例如最多 3 个挂单。

通过引入缓冲 Channel,咖啡馆增强了处理更多订单的能力。然而,仔细选择适当的缓冲区大小以维持客户合理的等待时间至关重要。毕竟,没有顾客愿意忍受过长的等待。有时,拒绝新订单可能比接受新订单但无法及时履行订单更容易接受。此外,在临时容器化 (Docker) 应用程序中使用缓冲 Channel 时务必谨慎,因为容器会随机重启,在这种情况下从 Channel 恢复消息可能是一项具有挑战性的任务,甚至近乎不可能。

Channels vs Blocking Queues

尽管本质上不同,Java 中的 Blocking Queue 用于线程之间的通信,而 Go 中的 Channel 用于 Goroutine 的通信,BlockingQueue 和 Channel 的行为有些相似。如果你熟悉BlockingQueue,理解 Channel 肯定会很容易。

常见使用场景

Channel 是 Go 应用程序中一项基本且广泛使用的功能,可用于多种用途。Channel 的一些常见用例包括:

  • Goroutine 通信:Channel 支持不同 Goroutine 之间的消息交换,允许它们进行协作,而无需直接共享状态。
  • 工作池:如上例所示,Channel 通常用于管理工作池,其中多个相同的工作者处理来自共享Channel 的传入任务。
  • 扇出、扇入:Channel 用于扇出、扇入模式,其中多个 Goroutine(扇出)执行工作并将结果发送到单个 Channel,而另一个 Goroutine(扇入)消耗这些结果。
  • 超时和截止日期:Channel与 select 语句结合可用于处理超时和截止日期,确保程序可以优雅地处理延迟并避免无限期的等待。

我将在其他文章中更详细地探讨 Channel 的不同用法。然而,现在,让我们通过实现上述咖啡馆场景并见证渠道如何运作来结束这篇介绍性博客。我们将探讨 Partier、Candier 和 Stringer 之间的交互,观察 Channel 如何促进它们之间的顺畅沟通和协调,从而实现咖啡馆内高效的订单处理和同步。

Show me your code!

package main

import (
"fmt"
"log"
"math/rand"
"sync"
"time"
) func main() {
ch := make(chan order, 3) wg := &sync.WaitGroup{} // More on WaitGroup another day
wg.Add(2) go func() {
defer wg.Done()
worker("Candier", ch)
}() go func() {
defer wg.Done()
worker("Stringer", ch)
}() for i := 0; i < 10; i++ {
waitForOrders()
o := order(i)
log.Printf("Partier: I %v, I will pass it to the channel\n", o)
ch <- o
} log.Println("No more orders, closing the channel to signify workers to stop")
close(ch) log.Println("Wait for workers to gracefully stop")
wg.Wait() log.Println("All done")
} func waitForOrders() {
processingTime := time.Duration(rand.Intn(2)) * time.Second
time.Sleep(processingTime)
} func worker(name string, ch <-chan order) {
for o := range ch {
log.Printf("%s: I got %v, I will process it\n", name, o)
processOrder(o)
log.Printf("%s: I completed %v, I'm ready to take a new order\n", name, o)
}
log.Printf("%s: I'm done\n", name)
} func processOrder(_ order) {
processingTime := time.Duration(2+rand.Intn(2)) * time.Second
time.Sleep(processingTime)
} type order int func (o order) String() string {
return fmt.Sprintf("order-%02d", o)
}

您可以复制此代码,对其进行调整并在 IDE 上运行它,以更好地了解通道的工作原理。

本文译自:https://medium.com/stackademic/go-concurrency-visually-explained-channel-c6f88070aafa

欢迎加我好友,交流可观测性相关话题或了解我们的商业产品,我的微信号:picobyte,加好友请备注您的公司和姓名

漫画图解 Go 并发编程之:Channel的更多相关文章

  1. 并发编程之J.U.C的第一篇

    并发编程之J.U.C AQS 原理 ReentrantLock 原理 1. 非公平锁实现原理 2)可重入原理 3. 可打断原理 5) 条件变量实现原理 3. 读写锁 3.1 ReentrantRead ...

  2. [转载]并发编程之Operation Queue和GCD

    并发编程之Operation Queue http://www.cocoachina.com/applenews/devnews/2013/1210/7506.html 随着移动设备的更新换代,移动设 ...

  3. Java并发编程之CAS

    CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术.简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替 ...

  4. 并发编程之wait()、notify()

    前面的并发编程之volatile中我们用程序模拟了一个场景:在main方法中开启两个线程,其中一个线程t1往list里循环添加元素,另一个线程t2监听list中的size,当size等于5时,t2线程 ...

  5. 并发编程之 Exchanger 源码分析

    前言 JUC 包中除了 CountDownLatch, CyclicBarrier, Semaphore, 还有一个重要的工具,只不过相对而言使用的不多,什么呢? Exchange -- 交换器.用于 ...

  6. 并发编程之 Condition 源码分析

    前言 Condition 是 Lock 的伴侣,至于如何使用,我们之前也写了一些文章来说,例如 使用 ReentrantLock 和 Condition 实现一个阻塞队列,并发编程之 Java 三把锁 ...

  7. python并发编程之Queue线程、进程、协程通信(五)

    单线程.多线程之间.进程之间.协程之间很多时候需要协同完成工作,这个时候它们需要进行通讯.或者说为了解耦,普遍采用Queue,生产消费模式. 系列文章 python并发编程之threading线程(一 ...

  8. python并发编程之gevent协程(四)

    协程的含义就不再提,在py2和py3的早期版本中,python协程的主流实现方法是使用gevent模块.由于协程对于操作系统是无感知的,所以其切换需要程序员自己去完成. 系列文章 python并发编程 ...

  9. python并发编程之asyncio协程(三)

    协程实现了在单线程下的并发,每个协程共享线程的几乎所有的资源,除了协程自己私有的上下文栈:协程的切换属于程序级别的切换,对于操作系统来说是无感知的,因此切换速度更快.开销更小.效率更高,在有多IO操作 ...

  10. python并发编程之multiprocessing进程(二)

    python的multiprocessing模块是用来创建多进程的,下面对multiprocessing总结一下使用记录. 系列文章 python并发编程之threading线程(一) python并 ...

随机推荐

  1. 力扣454(java&python)-四数相加 II(中等)

    题目: 给你四个整数数组 nums1.nums2.nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足: 0 <= i, j, k, l &l ...

  2. 力扣25(java&python)-K 个一组翻转链表(困难)

    题目: 给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表. k 是一个正整数,它的值小于或等于链表的长度.如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺 ...

  3. HMS数据库设置和优化

    简介:Hive Metastore (HMS) 是一种服务,用于在后端 RDBMS(例如 MySQL 或 PostgreSQL)中存储与 Apache Hive 和其他服务相关的元数据.本文主要分享H ...

  4. 顺丰科技 Hudi on Flink 实时数仓实践

    ​简介: 介绍了顺丰科技数仓的架构,趟过的一些问题.使用 Hudi 来优化整个 job 状态的实践细节,以及未来的一些规划. 本文作者为刘杰,介绍了顺丰科技数仓的架构,趟过的一些问题.使用 Hudi ...

  5. Solution Set - DP

    CF101E Candies and Stones Link&Submission. DP 的状态设计和转移都是显然的,唯一的问题在于需要输出方案,而这题卡空间.会发现如果用 bitset 存 ...

  6. JUC并发编程学习笔记(二)Lock锁(重点)

    Lock锁(重点) 传统的synchronized 传统的解决多线程并发导致的一些问题我们会使用synchronized关键字来解决,synchronized的本质就是队列.锁. Lock的实现类有: ...

  7. ansible系列(20)--ansible的变量详解

    目录 1. Ansible Variables 1.1 变量定义的方式 1.2 在playbook中定义变量 1.2.1 使用vars方式定义变量 1.2.2 使用vars_file方式定义变量 1. ...

  8. java8用Stream一行代码实现数据分组统计、排序、最大值、最小值、平均值、总数、合计

    getAverage(): 它返回所有接受值的平均值. getCount(): 它计算所有元素的总数. getMax(): 它返回最大值. getMin(): 它返回最小值. getSum(): 它返 ...

  9. three.js教程7-PBR材质与环境贴图CubeTextureLoader

    1.PBR材质 PBR是基于物理的渲染(physically-based rendering).模拟物体表面的反射算法. Three.js提供了两个PBR材质相关的类MeshStandardMater ...

  10. Stenciljs 学习之搭建项目

    框架介绍 stenciljs 是用于构建可重用.可扩展的设计系统的工具链.生成在每个浏览器中运行的小型.超快且 100% 基于标准的 Web Component. 更对介绍请参考官方网站 创建项目 使 ...