Обзор Go-Basic — управление параллелизмом — графический канал

Чан

Не общайтесь через разделяемую память. Рекомендуется ==совместно использовать память через связь==. == (Не общайтесь, разделяя память; вместо этого делитесь памятью, общаясь) == Это философский девиз параллелизма языка Go.

В отличие от использования примитивов параллелизма, таких как sync.Mutex. Хотя большинство проблем с блокировкой можно решить одним из двух методов: канальной или традиционной блокировкой, основная команда языка Go рекомендует использовать CSP.

Структурная схема

hchan_1_jiegou.png

Краткое описание:

  • bufБуферизованная структура канала, используемая для хранения буферизованных данных. это ==круговая очередь==
    • Структура хранения должна быть массивом
  • sendxи recvxиспользуется для записи отправленных или полученных в bufэтом循环队列index
  • lockявляется мьютексом.
  • recvqи очередь абстрактных структур ( ) sendq, которые получают ( <-channel) или отправляют ( channel <- xxx), соответственно. это двусвязный списокgoroutinesudog

Исходный код находится /runtime/chan.goв (текущая версия: 1.15). Структура есть hchan.

type hchan struct {
	qcount   uint           // total data in the queue
	dataqsiz uint           // size of the circular queue
	buf      unsafe.Pointer // points to an array of dataqsiz elements
	elemsize uint16
	closed   uint32
	elemtype *_type // element type
	sendx    uint   // send index
	recvx    uint   // receive index
	recvq    waitq  // list of recv waiters
	sendq    waitq  // list of send waiters

	// lock protects all fields in hchan, as well as several
	// fields in sudogs blocked on this channel.
	//
	// Do not change another G's status while holding this lock
	// (in particular, do not ready a G), as this can deadlock
	// with stack shrinking.
	lock mutex
}

type waitq struct {
	first *sudog
	last  *sudog
}
复制代码

Использование хчан

Создайте

ch := make(chan int, 3)
复制代码

hchan_2_create.png

Создать channelструктуру, которая фактически инстанцируется в памяти hchan, и вернуть указатель ch, который channelиспользуется при передаче между функциями во время нашего использования, именно поэтому указатель не используется при передаче функции channel, а напрямую Просто используйте channelего, потому что сам канал является указателем.

Отправить send(ch <- xxx) и recv(<- ch) для получения в канале

如果你想让goroutine以先进先出(FIFO)的方式进入一个结构体中,你会怎么操作? 加锁!对的!channel就是用了一个锁。hchan本身包含一个互斥锁mutex

channel中队列是如何实现的

channel中有个缓存buf,是用来缓存数据的(假如实例化了带缓存的channel的话)队列。我们先来看看是如何实现“队列”的。 还是刚才创建的那个channel

ch := make(chan int, 3)
复制代码

hchan_3_queue.png

当使用send (ch <- xx)或者recv ( <-ch)的时候,首先要锁住hchan这个结构体。

hchan_4_sendlock.png

然后开始send(ch <- xx).

ch <- 1
ch <- 1
ch <- 1
复制代码

hchan_5_sendstep.gif

然后是取recv ( <-ch)的过程,是个逆向的操作,也是需要加锁。

hchan_6_recv_lock.png

然后开始recv (<-ch)数据。

<-ch
<-ch
<-ch
复制代码

hchan_7_recvstep.gif

注意以上两幅图中bufrecvx以及sendx的变化,recvxsendx是根据循环队列(实现方式为数组) buf的变动而改变的。至于为什么channel会使用循环链表作为缓存结构,我个人认为是在缓存列表在动态的send和recv过程中,定位当前send或者recvx的位置、选择send的和recvx的位置比较方便吧,只要顺着链表顺序一直旋转操作就好。

缓存中按数组顺序存放,取数据的时候按数组顺序读取,符合FIFO的原则。

send/recv的细化操作

注意:缓存链表中以上每一步的操作,都是需要加锁操作的!

每一步的操作的细节可以细化为:

  • 加锁
  • 把数据从goroutine中copy到“队列”中
  • 释放锁

每一步的操作总结为动态图为:(发送过程)

hchan_8_send_recv_xihua.gif

接受过程

hchan_9_send_recv_xihua2.gif

  • 加锁
  • 将数据从队列中copy到goroutine中
  • 释放锁

所以不难看出,Go中那句经典的话:==Do not communicate by sharing memory; instead, share memory by communicating.==的具体实现就是利用channel把数据从一端copy到了另一端!

hchan_10_g1tog2.gif

该图解释

  • G1 将变量写入channel ch <- 1
  • G2 将变量从channel中读取出来 <-ch

channel缓存满了之后会发生什么?其中的原理是什么?

使用的时候,我们都知道,当channel缓存满了或者缓存为空的时候,我们继续send(ch <- xxx)或者recv(<- ch)会阻塞当前goroutine,但是,是如何实现的呢?

我们知道,Go的goroutine是用户态的线程(user-space threads),用户态的线程是需要自己去调度的,Go有运行时的scheduler去帮我们完成调度这件事情。关于Go的调度模型GMP模型我在此不做赘述,如果不了解,可以看原文作者的另一篇文章(Go调度原理)

goroutine的阻塞操作,实际上是调用send (ch <- xx)或者recv ( <-ch)的时候主动触发的,具体请看以下内容:

//goroutine1 中,记做G1

ch := make(chan int, 3)

ch <- 1
ch <- 1
ch <- 1
复制代码

hchan_11_full_block.png

且此时G1和M仍然处于绑定关系,仍然继续运行。

hchan_12_full_g1_block1.png

这个时候G1正在正常运行,当再次进行send操作(ch<-1)的时候,会主动调用Go的调度器,让G1等待,并让出M,让其他G去使用。

hchan_13_full_g1_stop_block2.png

  • G1 执行 ch <- 1
  • 判断ch是否已满,如果未满则写入buf否则抽象为sudog写入sendq - 参见 chansend 方法

image.png

同时G1也会被抽象成含有G1指针和send元素的sudog结构体保存到hchansendq中等待被唤醒。

hchan_14_full_g1_sendq_blok3.gif

那么,G1什么时候被唤醒呢?这个时候G2隆重登场。

hchan_15_full_g2_block4.png

G2 выполняет операцию recv p := <-ch, поэтому происходят следующие операции:

hchan_16_full_g2_recv_block5.gif

G2 берет данные из очереди кэша, а канал выталкивает G1 в очередь ожидания.

image.png

Поместите данные, отправленные G1 в это время, в кеш, затем вызовите планировщик Go, разбудите G1 и поместите G1 в готовую к выполнению очередь Goroutine.

hchan_17_full_g2_fork_g1_block6.gif

Что, если G2 сначала выполняет операцию recv?

Возможно, вы сможете изменить вышеуказанную линию мышления. первый:

hchan_18_first_recv_block7_1.png

В это время G2 будет активно вызывать планировщик Go, пусть G2 подождет, а M будет использоваться другими G.

image.png

Если на приведенном выше рисунке qcount равно 0, G2 также будет абстрагирован в структуру, содержащую указатели G2 и recvпустые элементы , и будет sudogсохранен для hchanожидания recvqпробуждения.

hchan_19_first_recv_recvq_block7.gif

В этот момент есть ровно один goroutineG1, который начинает проталкивать данные в канал ch <- 1. В этот момент произошло очень интересное:

hchan_20_first_recv_g2tog1_block8.gif

G1 не блокирует канал, а затем помещает данные в кеш, а напрямую копирует данные из G1 в стек G2. Этот метод очень хорош! В процессе пробуждения G2 больше не нужно запрашивать блокировку канала, а затем извлекать данные из кэша. Уменьшение копирования памяти и повышение эффективности.

image.png

Последующие вещи очевидны:

hchan_21_first_recv_g2tog1_2block9.gif

Ссылаться на

рекомендация

отjuejin.im/post/7080176445787471908