写点什么

如何设计 Go 语言中的 channel

用户头像
soolaugust
关注
发布于: 2020 年 09 月 25 日
如何设计Go语言中的channel

本文转自“雨夜随笔”公众号,欢迎关注



今天我们来谈一下GO语言中的一些设计理念,文章是一个系列文章,主要是讲述GO语言的一些好的设计理念。与其他文章不同的是,我将带着大家站在自己的角度来分析如果是自己来设计,会是什么样?以此来更好的理解为什么GO语言这么设计,是不是有其他更好的角度。



今天我们来介绍一下Go语言中的利器--channel。





设计背景

在设计之前,我们首先要了解的是为什么要设计这个东西,也就是背景或者说是需求是什么?



了解GO的都知道,GO相比于其他语言的优势在于先天集成了goroutine--也就是协程。相比于线程需要在内核空间和用户空间进行不断切换,协程只运行在用户空间,使得协程的上下文切换消耗比线程要低,而且响应更加迅速。那么现在问题来了,在我们拥有了协程之后,我们想要在协程之间交换信息怎么办?





这个就引入了我们今天要设计的东西--channel。

需求分析

那么当我们了解了设计的背景或者需求后,我们不要马上开始写代码,因为我们现在拿到的需求是模糊的,不清晰的。我们需要将需求细化或者说是明确化,这样我们才知道究竟要怎么写代码。



那么我们的目的是为了保证协程之间能够安全,高效的交换信息。首先是要安全,也就是协程使用channel是安全的,那么就引入了我们第一个需求:goroutine-safe



然后是交换信息,那么channel至少要保证能够保存并传输需要保存的信息,也就是第二个要求:store and pass values between goroutines。



好了,到这里我们初步的需求分析就结束了,看样子基本能够达到我们的要求。

详细设计

那么我们略过一般的概要设计,来进行channel的详细设计,首先我们需要确认channel的数据格式,试想一下我们的channel可能会存在多个goroutine同时使用的情况,所以为了保证安全,我们需要goroutine使用时进行加锁来保证占用。那么如果我们需要交换多个信息呢?我们肯定是希望能够排入队列中,然后执行完当前的信息后按照先入先出(FIFO)的方式处理余下的信息。我们我们这里得出了我们channel的数据格式--队列(Queue)以及并发控制锁(lock)。如下图:





这里我们就基本实现了channel的核心了,我们来看一下channel的使用过程:





那么我们是不是就完成了channel的设计了呢?当然不是,我们继续思考我们的需求,这次我们考虑一下我们设计的边界,首先会很容易想到队列满了或者队列空了怎么办?也就是如果发送的信息超过了我们的设置或者发出的信息都被接收端接收了。这个时候我们接下来应该怎么办?参考上面的我们肯定希望将这些暂存起来,然后等队列满足条件了再继续处理,这里我们也希望能够满足先入先出(FIFO)的方式。所以我们这里增加两个遍历来保存发送端的等待队列和接收端的等待队列,如下图:





我们可以看到队列里面每一个元素有两个数据:g(goroutinue)和e(element,即要处理的channel信息)。当条件满足时我们就可以将对应等待的元素从队列中取出。由此,我们基本上完成了channel的大部分设计,这个已经完全满足我们当初的需求了。

优化

那么现在我们再回过头来看我们的设计,有哪些可以优化的点呢?比如说现在我们接收端有个goroutine在等待,而我们刚好处理完所有的发送队列,这时候正好有个需要发送信息的goroutine,那么按照我们当前的设计,我们需要把发送的goroutine的信息放入buffer queue,然后再切换到接收端的goroutine,将信息进行处理。而goroutine的上下文切换时需要消耗资源的,那么我们能不能优化这个场景呢?当然大家也很容易想到,我们其实不需要把发送端的信息放入buffer queue,而直接将信息发送给等待队列的goroutine。这个就是第一个优化点,也就是直接发送





第二个是Go的设计理念:



Don't communicate by sharing memory; share memory by communicating.



因为sharing memory会发生资源争抢,为了避免这个一般都是要加锁。而我们现在的channel设计就是加锁队列,当然也是一种共享内存的方式。所以无可避免的是会有加锁解锁的消耗。这个就会成为一个瓶颈。那么我们可不可以实现lock-free(无锁)方式的channel呢?lock-free并不是真正的无锁,而是使用乐观锁来解决这个问题。这个答案目前Go社区也只是一些提案,并没有纳入到生产中。所以这个问题就交给了聪明的您们了,如果感兴趣可以参考文章最后的链接。

Go中的设计

那么当我们理清了我们的思路后,我们在回过头来看一下Go语言中的设计,和我们有哪些不一样的地方。

type hchan struct {
qcount uint // channel中的元素个数
dataqsiz uint // 缓冲区的长度
buf unsafe.Pointer // 缓冲区的指针地址
elemsize uint16 // channel中能够接受的元素大小
closed uint32 // channel是否关闭
elemtype *_type // channel中能够接受的元素类型
sendx uint // channel 的发送操作处理到的位置
recvx uint // channel 的接受操作处理到的位置
recvq waitq // 阻塞的接受操作goroutine双向链表
sendq waitq // 阻塞的发送操作goroutine双向链表
lock mutex // 锁
}

我们可以看出Go在设计上优化了这样几点:

  1. 更多的描述信息,包括qcount, dataqsiz, closed, elemsize, elemtype, sendx, recvx

  2. 使用指针来减少数据结构占用的资源和传递带来的消耗

  3. 使用双向链表来实现goroutine等待队列



这些都是为了达到生产可用的设计方式。我们也从中学习。



推荐阅读





发布于: 2020 年 09 月 25 日阅读数: 62
用户头像

soolaugust

关注

公众号:雨夜随笔 2018.09.21 加入

公众号:雨夜随笔

评论

发布
暂无评论
如何设计Go语言中的channel