Для упрощения синхронизации между горутинами в Go имеется пакет sync, который предоставляет ряд возможностей, в частности мьютексы. Мьютексы позволяют разграничить доступ к некоторым общим ресурсам, гарантируя, что только одна горутина имеет к ним доступ в определенный момент времени. И пока одна горутина не освободит общий ресурс, другая горутина не может с ним работать.
На уровне кода мьютекс представляет тип sync.Mutex. Для блокирования доступа к общему разделяемому ресурсу у мьютекса вызывается метод Lock(), а для разблокировки доступа - метод Unlock().
В какой ситуации нам могут помочь мьютексы? Рассмотрим следующую ситуацию:
package main import "fmt" var counter int = 0 // общий ресурс func main() { ch := make(chan bool) // канал for i := 1; i < 5; i++{ go work(i, ch) } // ожидаем завершения всех горутин for i := 1; i < 5; i++{ <-ch } fmt.Println("The End") } func work (number int, ch chan bool){ counter = 0 for k := 1; k <= 5; k++{ counter++ fmt.Println("Goroutine", number, "-", counter) } ch <- true }
Функция work сбрасывает значение переменной counter к нулю и в цикле последовательно увеличивает ее значение до 5. В функции main запускается четыре горутин work. Но какой в данном случае будет консольный вывод? Он может быть, например, таким:
Goroutine 3 - 1 Goroutine 3 - 2 Goroutine 3 - 3 Goroutine 3 - 4 Goroutine 3 - 5 Goroutine 2 - 1 Goroutine 2 - 6 Goroutine 2 - 7 Goroutine 2 - 8 Goroutine 2 - 9 Goroutine 1 - 1 Goroutine 1 - 10 Goroutine 1 - 11 Goroutine 1 - 12 Goroutine 1 - 13 Goroutine 4 - 1 Goroutine 4 - 14 Goroutine 4 - 15 Goroutine 4 - 16 Goroutine 4 - 17 The End
Несмотря на то, что в каждой горутине значение counter сбрасывается к 0, а затем увеличивается до 5, мы видим, что несколько горутин после сброса переменной работают совсем с другим значением. То есть при запуске горутин каждая из них получает значение переменной counter и начинает с ней работать. Пока одна горутина еще не закончила работу с counter в цикле, с этой же переменной начинает работать и другая горутина. То есть к одному и тому же разделяемому общему ресурсу - переменной counter одновременно работают сразу несколько горутин. Это может привести к некорректным результатам, как в данном случае.
С помощью мьютексов можно ограничить доступ к переменной таким образом, чтобы только одна горутина имела к ней монопольный доступ в один момент времени:
package main import ( "fmt" "sync" ) var counter int = 0 // общий ресурс func main() { ch := make(chan bool) // канал var mutex sync.Mutex // определяем мьютекс for i := 1; i < 5; i++{ go work(i, ch, &mutex) } for i := 1; i < 5; i++{ <-ch } fmt.Println("The End") } func work (number int, ch chan bool, mutex *sync.Mutex){ mutex.Lock() // блокируем доступ к переменной counter counter = 0 for k := 1; k <= 5; k++{ counter++ fmt.Println("Goroutine", number, "-", counter) } mutex.Unlock() // деблокируем доступ ch <- true }
Теперь функция work принимает указатель на мьютекс. С помощью вызова mutex.Lock()
мьютекс блокируется данной горутиной. Это значит, что к
последующему коду имеет доступ только та горутина, которая первая заблокировала мьютекс. Остальные горутины ждут пока, мьютекс освободится.
Далее горутина сбрасывает значение переменной counter к нулю и затем в цикле последовательно увеличивает его. В конце, когда все действия с общим ресурсом
уже выполнены, горутина освобождает мьютекс с помощью вызова mutex.Unlock()
. Ожидающие горутины получают сигнал, что мьютекс освободился, и одна из горутин
блокирует мьютекс и начинает выполнять действия с переменной counter. И так далее горутины последовательно захватывают и освобождают мьютекс. В итоге к следующей секции:
mutex.Lock() // блокируем доступ к переменной counter counter = 0 for k := 1; k <= 5; k++{ counter++ fmt.Println("Goroutine", number, "-", counter) } mutex.Unlock() // деблокируем доступ
будет иметь доступ только та горутина, которая первая заблокировала мьютекс. В итоге мы получим следующий результат:
Goroutine 1 - 1 Goroutine 1 - 2 Goroutine 1 - 3 Goroutine 1 - 4 Goroutine 1 - 5 Goroutine 4 - 1 Goroutine 4 - 2 Goroutine 4 - 3 Goroutine 4 - 4 Goroutine 4 - 5 Goroutine 3 - 1 Goroutine 3 - 2 Goroutine 3 - 3 Goroutine 3 - 4 Goroutine 3 - 5 Goroutine 2 - 1 Goroutine 2 - 2 Goroutine 2 - 3 Goroutine 2 - 4 Goroutine 2 - 5 The End