Операция отправления
в небуферизованный
канал блокирует горутину
до тех пор, пока другая горутина не
выполнит соответствующее получение
из того же канала, после чего значение становится переданным, и обе горутины
продолжаются. И наоборот, если первой
сделана попытка выполнить операцию получения
, принимающая горутина блокируется
до тех пор, пока другая горутина не выполнит отправление
значения в тот же канал.
Связь по небуферизованному каналу
приводит к синхронизации
операций отправления
и получения
. По этой причине
небуферизованные каналы
иногда называют синхронными
. Когда значение отправляется
в небуферизованный канал
,
получение
значения предшествует продолжению работы отправляющей горутины. При рассмотрении параллелизма, когда мы говорим,
что х
предшествует у
, мы не просто имеем в виду, что х
происходит по времени раньше, чем у
; мы подразумеваем,
что х
гарантированно поступает именно так и что все выполненные им предыдущие действия, такие как обновления переменных,
завершены и вы можете полностью на них полагаться.
Когда х
не предшествует y
, и не происходит после y
, мы говорим, что х
выполняется параллельно
с у
. Это не
означает, что х
и у
обязательно одновременны, просто мы не можем ничего утверждать об их последовательности. Как мы
увидим в следующей главе, чтобы избежать проблем, которые возникают, когда две горутины одновременно обращаются к одной
и той же переменной, определенные события во время выполнения программы необходимо упорядочивать.
Клиентская программа в разделе 8.3 копирует входные данные на сервер в своей главной горутине, так что клиентская
программа завершается, как только закрывается входной поток, даже если имеется работающая в фоновом режиме go-
подпрограмма. Чтобы перед выходом программа ожидала завершения фоновых горутин, для синхронизации двух горутин мы используем канал
(см. netcat3.go):
func main() {
conn, err := net.Dial("tcp", "localhost:8000")
if err != nil {
log.Fatal(err)
}
done := make(chan struct{})
go func() {
io.Copy(os.Stdout, conn) // Примечание: игнорируем ошибки
log.Println("done")
done <- struct{}{} // Сигнал главной горутине
}()
mustCopy(conn, os.Stdin)
conn.Close()
<-done // Ожидание завершения фоновой горутины
}
func mustCopy(dst io.Writer, src io.Reader) {
if _, err := io.Copy(dst, src); err != nil {
log.Fatal(err)
}
}
Когда пользователь закрывает стандартный входной поток, происходит возврат из функции mustCopy
, и главная
горутина вызывает conn.Close()
, закрывая обе половины сетевого подключения. Закрытие записывающей половины
соединения заставляет сервер увидеть признак конца файла
. Закрытие считывающей половины приводит к тому, что вызов
io.Copy
в горутине возвращает ошибку "чтение из закрытого соединения" (поэтому мы убрали запись журнала ошибок);
в упражнении 8.3 предлагается лучшее решение. (Обратите внимание, что инструкция go вызывает литерал функции — это
весьма распространенная конструкция.)
Перед завершением работы фоновая горутина заносит в журнал соответствующее сообщение, а затем отправляет значение
в канал done
. Главная горутина, до тех пор, пока не получит это значение, находится в состоянии ожидания. В
результате перед завершением работы программа всегда заносит в журнал сообщение "done"
.
Сообщения, пересылаемые через каналы, имеют два важных аспекта. Каждое сообщение имеет значение
, но иногда важен сам
факт передачи сообщения
и момент, когда эта передача происходит
. Когда мы хотим подчеркнуть этот аспект передачи
сообщений, мы называем их событиями
. Когда событие
не несет дополнительной информации, т.е. его единственная цель —
это
синхронизация
, мы подчеркиваем этот факт, используя канал с типом элементов struct{}
, хотя не менее распространено
применение для той же цели каналов с типом значений bool
или int
, поскольку выражение done <- 1
короче выражения
done <- struct{}{}
.
-
Операция отправления в небуферизованный канал блокирует горутину до тех пор, пока другая горутина не выполнит соответствующее получение из того же канала; это обеспечивает синхронизацию отправления и получения данных между горутинами. Пример:
ch := make(chan int) go func() { ch <- 42 // Отправление значения в канал; горутина блокируется, пока другая горутина не выполнит получение }() val := <-ch // Получение значения из канала; обе горутины продолжают работу
-
Небуферизованные каналы иногда называют синхронными, поскольку они обеспечивают синхронизацию операций отправления и получения; это может быть полезно для упорядочивания выполнения горутин и избегания проблем с параллельным доступом к переменным.
-
Если нужно передать событие без значения, используйте канал с типом элементов struct{} ; это подчеркивает, что важен только факт передачи сообщения. Пример:
done := make(chan struct{}) go func() { // Работа в фоновой горутине done <- struct{}{} // Сигнал главной горутине об окончании работы }() <-done // Ожидание завершения фоновой горутины
-
Использование каналов для синхронизации горутин обеспечивает явное взаимодействие и гарантирует, что определенные действия завершены до продолжения работы других горутин; это помогает избежать ошибок, связанных с параллельным доступом к общим данным и состоянию.
-
При реализации клиент-серверной связи с использованием горутин и каналов важно использовать синхронизацию для корректного завершения работы программы и предотвращения потери данных. Пример кода:
func main() { conn, err := net.Dial("tcp", "localhost:8000") if err != nil { log.Fatal(err) } done := make(chan struct{}) go func() { io.Copy(os.Stdout, conn) // Примечание: игнорируем ошибки log.Println("done") done <- struct{}{} // Сигнал главной горутине }() mustCopy(conn, os.Stdin) conn.Close() <-done // Ожидание завершения фоновой горутины }