«Плюсы и минусы» внутренней реализации каналов Голанга и связанных с ним операций.

Параллелизм в Golang - это гораздо больше, чем просто синтаксис.

Это шаблон дизайна.

Шаблон, который является повторяемым решением часто встречающейся проблемы при работе с параллелизмом, потому что даже

Необходимо синхронизировать параллелизм.

И Go полагается на модель параллелизма, называемую CSP (Communicating Sequential Processes), для достижения этой модели синхронизации через канал. Основная философия Go для параллелизма:

Не общайтесь, разделяя память; вместо этого поделитесь памятью, общаясь.

Но Go также верит, что вы поступаете правильно. Итак, остальная часть сообщения попытается раскрыть этот конверт философии Go и того, как каналы - используя очередь для достижения того же.

Что нужно, чтобы стать каналом.

func goRoutineA(a <-chan int) {
    val := <-a
    fmt.Println("goRoutineA received the data", val)
}
func main() {
    ch := make(chan int)
    go goRoutineA(ch)
    time.Sleep(time.Second * 1)
}

Таким образом, ответственность канала в Go заключается в том, чтобы снова сделать горутину работоспособной, которая заблокирована на канале при получении или отправке данных.

Если вы не знакомы с Go Scheduler, прочтите это хорошее введение. Https://morsmachine.dk/go-scheduler

Структура канала

В Go «структура каналов» является основой передачи сообщений между Goroutine. Итак, как выглядит эта структура канала после того, как мы ее создали?

ch := make(chan int, 3)

Выглядит хорошо, хорошо. Но что это на самом деле означает? и откуда канал получает свою структуру. Прежде чем продолжить, давайте рассмотрим несколько важных структур.

hchan структура

Когда мы пишем, make(chan int, 2)channel создается из структуры hchan, которая имеет следующие поля.

Давайте добавим описания к нескольким полям, которые мы встретили в структуре канала.

dataqsize Размер буфера, упомянутого выше, то есть make (chan T, N), тогда.

elemsize Размер канала, соответствующий одному элементу.

buf - это круговая очередь , в которой фактически хранятся наши данные. (используется только для буферизованного канала)

closed Указывает, находится ли текущий канал в закрытом состоянии. После создания канала в этом поле устанавливается значение 0, то есть канал открыт; вызовом close, чтобы установить его в 1, канал закрывается.

sendx и recvx - это поле состояния кольцевого буфера, которое указывает текущий индекс массива поддержки буфера, откуда он может отправлять и получать данные.

Очереди ожидания recvq и sendq, которые используются для хранения заблокированных горутин при попытке чтения данных на канале или при попытке отправить данные из канала.

lock Для блокировки канала для каждой операции чтения и записи, поскольку отправка и получение должны быть взаимоисключающими операциями.

Так что это за судог?

структура Sudog

Судоги представляют собой горутины.

Давайте попробуем шаг за шагом осмыслить структуру каналов. Важно иметь четкое представление об этом, так как это то, что дает каналу силу в Go.

Какая будет структура канала перед строкой № 22?

Обратите внимание на выделенные строки 47 и 48 выше. Помните описание recvq сверху

recvq используются для хранения заблокированных горутин, которые пытаются читать данные из канала.

В нашем примере кода перед строкой 22 есть две горутины (goroutineA и goroutineB), пытающиеся прочитать данные из канала ch.

Поскольку перед строкой 22 на канале нет данных, которые мы поместили в канал, поэтому обе горутины, заблокированные для операции приема, были заключены в структуру sudog и присутствуют в recvq канала.

Судоги представляют собой горутины.

recvq и sendq - это в основном связанный список, который выглядит примерно так, как показано ниже

Эти структуры действительно важны,

Посмотрим, что произойдет, когда мы попытаемся передать данные на канал ch

Отправить шаги операции c ‹- x

Основные типы операций отправки на канале

  1. отправка по нулевому каналу

Если мы отправляем по нулевому каналу, текущий goroutine приостановит свою работу.

2 . отправка по закрытому каналу.

Если мы попытаемся отправить данные по закрытому каналу, наша горутина паникует.

3 . На канале заблокирована горутина: данные отправляются непосредственно в горутину.

Именно здесь структура recvq играет такую ​​важную роль. Если в recvq есть какая-либо горутина, это ожидающий получатель, и текущая операция записи в канал может напрямую передать значение этому получателю. Реализация функции отправки.

Обратите внимание на строку номер 396 goready(gp, skip+1). Goroutine, который был заблокирован во время ожидания данных, снова стал работоспособным путем вызова goready,, и планировщик go снова запустит горутину.

4 . Буферизованный канал, если в настоящее время есть место для hchan.buf: поместите данные в буфер.

chanbuf(c, i) обращается к соответствующей области памяти.

Определите, есть ли на hchan.buf свободное место, сравнив qcount и dataqsiz. ** Поместите элемент в очередь, скопировав область, на которую указывает указатель ep, в кольцевой буфер для отправки ** и настройте sendx и qcount.

5. Файл hchan.buf заполнен.

Создать объект goroutine в текущем стеке

acquireSudog, чтобы перевести текущую горутину в состояние парковки, а затем добавить эту горутину в sendq канала.

Отправить сводку операции

  1. заблокировать всю структуру канала.
  2. определяет пишет. Попробуйте recvq взять ожидающую горутину из очереди ожидания, а затем передать элемент для записи непосредственно в горутину.
  3. Если recvq пуст, определите, доступен ли буфер. Если возможно, ** copy ** (typedmemmove копирует значение типа t в dst из src.`) данные из текущей горутины в буфер.
    _typedmemmove _ внутренне использует memmove - memmove () используется для копирования блока памяти из одного места в другое.
  4. Если буфер заполнен, то записываемый элемент сохраняется в структуре выполняющейся в данный момент горутины, а текущая горутина помещается в очередь в sendq и приостанавливается из среды выполнения.

Пункт № 4 действительно интересен.

Если буфер заполнен, то записываемый элемент сохраняется в структуре выполняющейся в данный момент горутины.

Прочтите его еще раз, потому что именно поэтому небуферизованный канал на самом деле называется «небуферизованным», хотя структура «hchan» имеет связанный с ним элемент «buf». Потому что для небуферизованного канала, если нет получателя и если вы попытаетесь отправить данные, данные будут сохранены в elem структуры sudog. (Верно и для буферизованного канала).

Приведу пример, чтобы более подробно прояснить пункт 4. Предположим, у нас есть приведенный ниже код.

Какой будет структура времени выполнения chan c2 в строке номер 10?

Вы можете видеть, что даже если мы поместили в канал значение int 2, buf не имеет значения, но оно будет в структуре sudog горутины. Поскольку goroutineA попытался отправить значение на канал c2, а получатель не был готов, goroutineA будет добавлен в sendq список канала c2 и будет запаркован при блокировке. Мы можем изучить структуру выполнения блокировки sendq, чтобы проверить.

Теперь, когда у нас есть обзор операции отправки по каналу, что происходит, когда мы отправляем значение в наш пример кода выше в строке 22.

ch <- 3

Поскольку recvq канала имеет горутину в состоянии ожидания, он удалит из очереди первый судог и поместит данные в эту горутину.

Помните, что вся передача стоимости по текущим каналам происходит с копией значения.

Что будет на выходе вышеуказанной программы? Просто помните, что канал работает с копией значения.
Итак, в нашем случае канал скопирует значение по адресу g в свой буфер.

Не общайтесь, разделяя память; делитесь памятью, общаясь.

Вывод

&{Ankur 25}
modifyUser Received Value &{Ankur Anand 100}
printUser goRoutine called &{Ankur 25}
&{Anand 100}

Получить шаги операции ‹- ch

Это почти то же самое, что и операции отправки

Выбирать

Мультиплексирование на нескольких каналах.

  1. Операции являются взаимоисключающими, поэтому необходимо установить блокировки на всех задействованных каналах в выбранном случае, что выполняется путем сортировки обращений по адресу Hchan для получения порядка блокировки, поэтому что он не блокирует мьютексы сразу всех задействованных каналов.
sellock(scases, lockorder)

Каждый scase в массиве scases - это структура, которая содержит тип операции в текущем случае и канал, на котором она работает.

kind Тип операции в текущем случае, может быть CaseRecv, CaseSend и CaseDefault.

2. Вычислите порядок опроса, чтобы перемешать все задействованные каналы, чтобы обеспечить псевдослучайную гарантию, и поочередно просмотрите все обращения в соответствии с порядком опроса один за другим, чтобы увидеть, готов ли какой-либо из них к обмену данными. Этот порядок опроса заставляет операции выбора не обязательно следовать порядку, объявленному в программе.

3. Оператор select может возвращаться, пока существует операция канала, которая не блокируется, даже без необходимости касаться всех каналов, если выбранный готов.

3. Если ни один канал в настоящее время не отвечает и нет оператора по умолчанию, текущий g должен в настоящее время зависать в соответствующей очереди ожидания для всех каналов в соответствии с их случаем.

sg.isSelect - это то, что указывает на то, что горутина участвует в операторе выбора.

4. Операции приема, отправки и закрытия во время операции выбора аналогичны общим операциям приема, отправки и закрытия для каналов.

Заключение

Каналы - очень мощный и интересный механизм в Go. Но чтобы использовать их эффективно, нужно понимать, как они работают. Надеюсь, эта статья объяснит фундаментальный принцип работы каналов в Go.

Хотите узнать больше о Go? Присоединяйтесь к нам в Go Study Group

Учебная группа - отличный способ не только скрываться и учиться, но и знакомиться с другими людьми в сообществе. Всем добро пожаловать!

Узнал что-то? Хлопайте 👏, чтобы помочь другим найти эту статью.

✉️ Подпишитесь на рассылку еженедельно Email Blast 🐦 Подпишитесь на CodeBurst на Twitter , просмотрите 🗺️ Дорожная карта веб-разработчиков на 2018 год и 🕸️ Изучите веб-разработку с полным стеком .