Golang学习实战笔记-基础 > golang底层实现原理
go channel 内部运行机制剖析

摘要

channel为goroutines提供了一种简单的通信机制,并为构建复杂的并发模式提供了强大的结构。我们将深入研究channel和channel操作的内部工作原理,包括运行时调度程序和内存管理系统如何支持它们。

我们中相当多的人正在使用Go的并发功能。channel不仅有用,而且很有趣。

我们可以很容易使用channel来实现一个简单的任务队列。


channel之所以有趣,是因为:

  • channel很安全

  • 通道在goroutine之间传送消息

  • FIFO语义

  • 通道导致阻塞和解除阻塞

即使我们了解所有这些属性,您仍然需要花一点时间来欣赏它,并且想要了解它的工作原理。

这个话题是关于:

  • 创建channel(hchan结构)

  • 发送和接收(goroutine调度)

  • 总结(为什么这样设计)

创建channel

首先,您需要创建一个channel。make可以创建缓冲和非缓冲channel。本篇文章话题主要是关于缓冲channel的。

创建容量为3的带缓存channel:ch:= make(chan Task,3)

  • Goroutine安全

  • 存储最多容量元素并提供FIFO语义

  • 在goroutine之间发送值

  • 会造成封锁/解除封锁

make chan在堆上分配hchan结构并对其进行初始化,并返回指向该结构的指针。这就是为什么我们可以在函数之间传递channel。

使用channel

发送和接收如何工作?

  • G1是生产者,G2是消费者。

  • G1首先发送task到channel, 获取锁并将task拷贝到队列中。

  • 然后G2出现并进行逆运算, 它获取锁并出队将队列中task值复制到G2的堆栈中。

  • 复制为我们提供了安全性,该channel受互斥锁保护,没有其他共享内容。值被复制。

阻塞/非阻塞止如何工作?

假设G1继续发送,并且处理G2需要很长时间。通道已满时,G1的执行暂停;goroutine是如何暂停工作?

  • Goroutine是由调度程序控制 。

  • Goroutine是用户空间线程,是由Go创建和管理。与OS线程相比它们是轻量级的。

  • Go调度程序是一个M:N调度程序:几个(M)很少的OS线程和很多个(N)Goroutine。调度程序负责在有限数量的OS线程上调度goroutine。

暂停goroutine

当需要暂停goroutine时,chan调用调度程序以驻留G1,调度程序所做的就是将G1从运行更改为等待。并在OS线程上安排另一个goroutine。

  • 这对性能好, 我们还没有停止OS线程,而是通过上下文切换到了另一个goroutine,这代价并不昂贵。

  • 但channel不再满时,我们需要恢复已暂停的goroutine。

恢复goroutines

  • Waiting goroutine结构体有一个指向它正在等待的元素的指针。

  • G1为自己创建了一个sudog,将其放入通道的等待队列中。Sudog将在恢复G1时被使用。

  • 当通道不再满时,G1的操作是弹出sudog。G1再次设置为可运行。调度器调度G1,G1恢复。

(上图还解释了当G2需要停止并再次恢复时会发生的情况)

直接发送

G1向channel写入数据时它需要获取锁。但是实际上运行时可以将数据直接复制到接收者堆栈,因此成本更低。G1直接写入G2的堆栈,而不必获取任何锁。

恢复时,G2不需要获取channel的锁和复制channel数据到G2的接收对象。这也意味着减少了一个内存副本。

总结

为什么channel是这样实现的?有两点要点:

简单性:与无锁实现相比,具有锁的队列更容易实现。

性能:goroutine唤醒路径是无锁的,并且可能减少了内存副本。

在channel的实现中,在简单性和性能之间进行了明智的权衡