#20.1 golang Каналы - с буфером и без, состояние блокировки, взаимоблокировки deadlock
Primary tabs
Forums:
Каналы (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)
}
-- здесь мы:
- объявили небуфферизированный канал (т.к. не указали явно размер буффера)
- инициализировали его через make()
- внутри отдельной горутины проверли в него запись
- а внутри основной горутины считали значение
Как каналы блокируют горутины, 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 значения сразу блокируют горутину, небуферизированный при записи последнего доступного в буфере значения не блокирует, а блокирует только при попытке "записать еще больше".
Главный концептуальный момент в том, что:
- буфер это не "размер очереди" для записи, а скорее "карман" для еще не считанных из канала значений - они там лежат "до востребования"
- как только буфер заполнен, наш канал на запись начинает вести себя как и небуферизированный - т.е. стоит только записать ещё одно значение и горутина блокируется.
-- также можно сказать, что буфер это просто способ избежать обязательной передачи значения "из рук в руки" (синхронной работы горутин читающей и пишущей в канал) и работает он только пока буфер не заполнен, а далее поведение такое же как и у канала без буфера.
Источники
- Log in to post comments
- 91 reads
vedro-compota
Thu, 04/16/2026 - 21:35
Permalink
уточнить
Разобрать:
_____________
матфак вгу и остальная классика =)