Синхронизированные горутины (часть I)
Предположим, что программа Go запускает две горутины:
package main import ( "fmt" "sync" ) func main() { var v int var wg sync.WaitGroup wg.Add(2) go func() { v = 1 wg.Done() }() go func() { fmt.Println(v) wg.Done() }() wg.Wait() }
Обе горутины работают с общей переменной v. Один из них устанавливает новое значение (записывает), а второй печатает эту переменную (читает).
WaitGroup из пакета синхронизации используется для ожидания завершения двух неосновных горутин. В противном случае не было бы никакой гарантии, что какая-либо из этих горутин вообще запустится.
Поскольку горутины являются независимыми параллельными задачами, не существует неявного упорядочивания между выполняемыми ими операциями. В приведенном выше примере не ясно, будет ли напечатано 0
или 1
. На выходе будет 1
, когда в момент запуска fmt.Println
другая горутина уже выполнила оператор присваивания v = 0
. Пока неизвестно, пока программа действительно не запустится. Другими словами, оператор присваивания и вызов fmt.Println
не упорядочены - они действуют одновременно.
Плохо, если мы не сможем определить, что делает программа, посмотрев на ее исходный код. Спецификация Go вводит частичный порядок (происходит до) операций с памятью (чтения или записи). Этот порядок позволяет понять, что делает программа. Некоторые механизмы языка дополнительно позволяют программисту устанавливать порядок операций.
Внутри одной горутины все операции упорядочены так, как они помещены в исходный код:
wg.Add(2) wg.Wait()
Вызовы функций из приведенного выше примера, поскольку они помещены в одну и ту же горутину, упорядочены - wg.Add(2)
происходит до wg.Wait()
.
1. Каналы
Связь с использованием каналов является основным методом синхронизации. Отправка значения в канал происходит до его получения :
var v int var wg sync.WaitGroup wg.Add(2) ch := make(chan int) go func() { v = 1 ch <- 1 wg.Done() }() go func() { <-ch fmt.Println(v) wg.Done() }() wg.Wait()
Новое - канал ch. Поскольку получение происходит после отправки значения в канал, а отправка значения происходит после присвоения v, то вышеуказанная программа всегда печатает 1
:
установить v → отправить в ch → получить от ch → print v
Первая и третья стрелки являются следствием упорядочивания в одной и той же горутине. С помощью канала связи введена вторая стрелка. В конечном итоге упорядочиваются операции, распределенные между двумя горутинами.
2. пакет синхронизации
Пакет sync предоставляет примитивы синхронизации. Одно из них, которое могло бы решить нашу проблему - Mutex. При наличии переменной lock типа sync.Mutex гарантировано, что второй вызов lock.Lock()
произойдет после первого вызова lock.Unlock()
. 3-й вызов lock.Lock()
происходит после 1-го и 2-го вызовов lock.Unlock()
. Вообще говоря, n -й вызов lock.Lock()
происходит после m -го вызова lock.Unlock()
для m ‹n. Давайте посмотрим, как использовать эти знания для решения нашей проблемы синхронизации:
var v int var wg sync.WaitGroup wg.Add(2) var m sync.Mutex m.Lock() go func() { v = 1 m.Unlock() wg.Done() }() go func() { m.Lock() fmt.Println(v) wg.Done() }() wg.Wait()
В будущих статьях будет рассказано больше о связи с каналами (т.е. как это работает с буферизованными каналами), но также будет подробно объяснено, что предоставляет пакет sync.
Нажмите ❤ ниже, чтобы помочь другим узнать эту историю. Если вы хотите получать обновления о новых сообщениях, подписывайтесь на меня.