前情提要是这样的:
我昨天参加了我非常喜欢的某公司的全栈开发工程师的面试, 在介绍了我各种过往奇奇怪怪的(真的是非常奇怪的,面试官听了以后直皱眉😂)项目之后, 面试官开始考核我的一些实战能力。 然后上来的第一道代码设计的题目就把我考到了,要我用协程实现多生产多消费,并且能控制协程创建退出。
可能是我 Golang 只接触了一个月左右的时间,
虽然之前确实有一次被考到 channel 的用法(但复盘的时候也就是简单补习了一下 make channel)。
这次上来直接动手要写代码,还是很多反应不过来,我直接就寄了…
查了一会 channel 的用法,后面憋出几行又发现为了不让进程过早退出还要使用 waitGroup 这些😇。
然后上面说到的模型确实比较重要的,毕竟我简历上也吹嘘自己掌握各种异步任务设计…
结果就是啪啪打脸。
于是在这场被虐的非常惨烈的面试之后,我决定把这样一个生产消费模型好好补一下。
最后补习完了以后发现,大量的内容还是跟 Golang 的 channel 用法有关,那么这篇文章就主要对其做一个梳理。
基础
我不会讲太多,因为很多地方可以查到,介绍的肯定也更详细。 这里只是提供一个较快的了解。
channel
首先对 channel 需要掌握它的类型声明、创建和操作方法。
channel的类型声明
在Golang当中,我们通过 [VARIABLE NAME] <-chan | chan | chan<- [TYPE] 这样的句式来声明一个 channel 和它内部的数据类型。
这里 chan 有点像一个前缀的修饰词,可能由于是关键字的原因,在用法上和普通泛型上有区分。
我认为关键是要使用象形的记忆方式,把 chan 本身就想象成一个管道,你可以在 chan 的两侧用 <- 来标记 chan 在这个作用域中的可以使用的端口是写端还是读端,没有标记时代表允许双端的操作
channel的创建
通过 make 我们可以创建一个 channel ,这里值得注意的是 make 分配的容量是根据对象的原子个数分配的。
像 string 类型,分配的是字符串的长度,这里是容易理解错的。
但如果你是创建的一个 struct,那分配的容量是“多少个struct”。
channel的操作
chan 最基本的操作就是写值和取值操作。
写值的语法是 [CHANNEL] <- [VARIABLE] | [VALUE],是象形的操作方式。
取值的语法是 [VARIABLE] := <-[CHANNEL],代表从写端取值,
此外还有和 range 一起使用的一些语法糖(这里就不过多介绍了)。
Context
Context 的中文含义是上下文,这类设计其实在很多服务框架中有做到。
像在Rust的一些Web框架中,会有 app.data 的设计,这样做的目的主要就是方便线程/协程之间实现数据的共享。
在Golang当中,我认为 Context 也起到了类似的作用。
但在这样一个功能的基础上,Context 还提供了一些特殊字段和控制方法,用来实现对协程的控制。
最重要的一种用法如下:
| |
我们通过 ctx.Done 这个管道来判断协程是否需要终止,
这个管道的消息可以通过设置 TimeOut 或者手动 cancel 等方法来发送。
具体可以查阅相关的资料。
Practice
最后我们来提一下多生产多消费这个模型的实践。
思路其实很简单,我们需要创建一个 channel 来作为消息队列,另外我们还需要一个 Context 来控制协程的退出。
我们首先定义消息的格式,并准备 channel 和 Context。
| |
dataChannel 是用来生产的消息队列,countChannel 是生产者之间用来协同控制消息号的。
由于 countChannel 的读写会发生在一个生产者当中,我们需要设置一个大小为1的缓冲,
每次生产者进行生产之前,生产者会先读取缓冲中的值,并把最新的消息号更新。
最后结合 Context 的基本用法,我们得到的核心逻辑如下:
Producer 核心逻辑
| |
Consumer 核心逻辑
| |
