#20.1 golang Каналы - с буфером и без, состояние блокировки, взаимоблокировки deadlock

Каналы (chan) — это типизированные очереди (FIFO), которые позволяют горутинам обмениваться данными без явных блокировок.

В языке Go канал имеет определенный тип, то есть тип значений, которые он может передавать из одной горутины в другую. Для определения канала применяется ключевое слово chan:

chan тип_элемента

-- фактически так объявляется что-то вподе составного типа, т.е. на пример "канал целых чисел".

Ниже мы посмотрим как это работает в коде.

Канал без буфера (небуфферизированные каналы)

Рассмотрим такой пример кода:

package main

import (
	"fmt"
)

func main() {

	var intChannel chan int
	fmt.Println("До иницилаизации:", intChannel) // выведет nil

	/* Канал необходимо инициализировать перед использованием
	для чего используем функцию make(),
	которая выделяет память.
	   При этом для небуфферизированных каналов
	мы не указываем размер буфера */
	intChannel = make(chan int)

	go func() {
		intChannel <- 5 // пишем значение в канал
	}()

	val := <-intChannel
	fmt.Println("Значение из канала:", val)
}

-- если использовать обычную функцию вместо анонимной, то то же самое можно переписать так:

import (
	"fmt"
)

func doIt(value int, channel chan int) {
	channel <- value // пишем значение в канал
}
func main() {

	var intChannel chan int
	fmt.Println("До иницилаизации:", intChannel) // выведет nil

	intChannel = make(chan int)

	// запускаем еще одну горутину
	// для записив  канал
	go doIt(5, intChannel)

	val := <-intChannel
	fmt.Println("Значение из канала:", val)
}

-- здесь мы:

  1. объявили небуфферизированный канал (т.к. не указали явно размер буффера)
  2. инициализировали его через make()
  3. внутри отдельной горутины проверли в него запись
  4. а внутри основной горутины считали значение

Как каналы блокируют горутины, deadlock

Горутина блокируется, если:

  • Была запись в канал и он заполнился (если буфера нет, то достататочно записть одно значение) - начинаем ждать пока кто-то (другая горутина) считает данные
  • Вызвано чтение, но канал пуст - начинаем ждать пока кто-то не запишет туда значение

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

На практике это означает, что если мы напишем код вроде:

package main

import "fmt"

func main() {
    ch := make(chan int) // небуферизованный канал
    ch <- 10         // main блокируется здесь — ждёт чтения
    fmt.Println(<-ch) // никогда не выполнится
}

-- получим фатальную ошибку:

all gorutines are sleep

-- она возникает из-за взаимоблокировки (deadlock, у нас только одна горутина и она заблокирована), именно поэтому в коде выше мы делали запись в канал в отдельной горутине.

Блокировка при неправильно порядке чтения/записи

В сответствии с правилами выше, мы можем попасть в состояние блокироки в ситуации, когда заполнили канал, но не запустили заранее читающую горутину:

import (
	"fmt"
	"sync"
)

func read(channel chan int, wg *sync.WaitGroup) {
	fmt.Println(<-channel) // читаем значение
	wg.Done()              // сигнализируем, что горутина завершилась
}

func main() {

	var wg sync.WaitGroup
	channel := make(chan int)
	wg.Add(1) // будем ждать выполнения еще 1 горутины

	channel <- 5 // пишем в канал БЛОКИРОВКА!
	// читать некому,
	// вызов читающей горутины идет ниже:
	go read(channel, &wg) // запускаем горутину

	wg.Wait() // ждем завершения горутины
}

-- чтобы это исправить, "читающую" горутину нужно запустить заранее (она кстати, сначала тоже заблокируется, но потом главная горутина забросит в канал значения для чтения и процесс пойдет):

import (
	"fmt"
	"sync"
)

func read(channel chan int, wg *sync.WaitGroup) {
	fmt.Println(<-channel) // читаем значение
	wg.Done()              // сигнализируем, что горутина завершилась
}

func main() {

	var wg sync.WaitGroup
	channel := make(chan int)
	wg.Add(1) // будем ждать выполнения еще 1 горутины

	go read(channel, &wg) // запускаем горутину
	channel <- 5          // пишем в канал

	wg.Wait() // ждем завершения горутины
}

Также при операции чтения канал заблокирует читающую горутину, если в него ничего не записано.

Канал с буфером

Ниже пример буферизованного канала, такой канал может переполниться, но отправка в нем блокируется только при заполнении буфера, а получение — только при пустом буфере.

package main

import (
	"fmt"
)

func main() {
	// буфер на 2 элемента
	ch := make(chan string, 2)

	ch <- "A" // не блокируется
	ch <- "B" // не блокируется
	// ch <- "C" // заблокируется (буфер уже заполнен)

	fmt.Println(<-ch) // "A" (буфер: ["B"])
	fmt.Println(<-ch) // "B" (буфер: [])
	// fmt.Println(<-ch) блокируется (буфер пуст)
}

- канал с буфером начинает блокировать горутины так же, как и небуферизированный, если:

  • для чтения канал пуст
  • или, в случае записи - уже буфер заполнен (т.е. мы пишем лишнее значение, если же пишем последнее вмещающееся значение, то блокировки не будет)

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

Отличия в логике блокироки горутины при записи

Может возникнуть вопрос: почему каналы без буфера, при записи туда 1 значения сразу блокируют горутину, небуферизированный при записи последнего доступного в буфере значения не блокирует, а блокирует только при попытке "записать еще больше".

Главный концептуальный момент в том, что:

  1. буфер это не "размер очереди" для записи, а скорее "карман" для еще не считанных из канала значений - они там лежат "до востребования"
  2. как только буфер заполнен, наш канал на запись начинает вести себя как и небуферизированный - т.е. стоит только записать ещё одно значение и горутина блокируется.

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

Источники

Key Words for FKN + antitotal forum (CS VSU):

vedro-compota's picture

Разобрать:

  1. уточнить почему каналы передаются в функции без получения указателя (готово, ответ тут - потому там неявно передается указатель)
  2. почему небуф каналы блокируются сразу, а не когда туда пытаются записать второе значение - готово, разобрано выше в уроке

_____________
матфак вгу и остальная классика =)