В этом посте рассмотрим задачу синхронизации go-процедур (goroutine). Дан код:
package main
import (
"fmt"
)
func main() {
hosts := []string{"ahost", "bhost", "chost"}
for i : range hosts {
go func(){
fmt.Println(hosts[i])
}()
}
}
Вопрос: что выведет данный код? Ответ: он вообще не скомпилируется. Это видно невооруженным взглядом - здесь банальная ошибка в синтаксисе range - range для среза будет возвращать 2 значения - индекс элемента и содержимое элемента, а также оператор присваивания написан неверно - вместо := написано :. Хорошо, исправим:
package main
import (
"fmt"
)
func main() {
hosts := []string{"ahost", "bhost", "chost"}
for i, _ := range hosts {
go func(){
fmt.Println(hosts[i])
}()
}
}
Вопрос: что выведет код сейчас? Ответ: ничего. Почему? Потому что запустив 3 goroutine основной поток исполнения функции main не будет ждать их выполнения и выполнит возврат, поэтому в консоль не будет ничего напечатано. Необходимо чтобы были напечатаны все три значения hosts - по одному на каждую goroutine. Попробуем исправить: здесь у нас появляется выбор, поскольку получить такой результат можно разными способами. Рассмотрим использование sync.WaitGroup:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
hosts := []string{"ahost", "bhost", "chost"}
for i, _ := range hosts {
wg.Add(1)
go func() {
fmt.Println(hosts[i])
wg.Done()
}()
}
wg.Wait()
}
Вопрос: что выведет код сейчас? Ответ: три раза chost. Почему? Потому что каждая goroutine, хотя и является замыканием и может использовать переменную i, определенную вне анонимной функции, запускаемой как go-процедура (goroutine), но не запоминает значения i, а на момент исполнения каждой из go-процедур цикл range уже завершился и в i сохранилось последнее значение 2, поэтому все go-процедуры печатают значение hosts[2]. Исправим, чтобы получить значение i, которое было во время запуска go-процедуры:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
hosts := []string{"ahost", "bhost", "chost"}
for i, _ := range hosts {
wg.Add(1)
go func(i int) {
fmt.Println(hosts[i])
wg.Done()
}(i)
}
wg.Wait()
}
Теперь будут выведены все 3 значения hosts. go-процедуры будут выполняться конкурентно, но порядок вывода не гарантируется. Похожий на первый взгляд результат мы можем получить используя для синхронизации небуферизованный канал:
package main
import (
"fmt"
)
func main() {
// Создаем небуферизованный канал
ch := make(chan int)
hosts := []string{"ahost", "bhost", "chost"}
for i, _ := range hosts {
go func(i int) {
fmt.Println(hosts[i])
// Получаем сообщение из канала
<-ch
}(i)
// Отправляем 0 в канал
ch <- 0
}
}
Теперь также будут выведены все 3 значения hosts. За счет того что прием из небуферизованного канала происходит до завершения отправки по небуферизованному каналу мы гарантируем что последняя go-процедура будет выполнена и все значения будут напечатаны.
Но у данного подхода есть проблема - мы потеряли конкурентность в исполнении go-процедур - теперь они запускаются последовательно и каждая следующая go-процедура, ждет завершения предыдущей, поскольку в range цикле запись в небуферизованный канал может быть выполнена только когда канал пуст. Можно вернуть конкурентность, для этого используем буферизованный канал с размером равным размеру hosts - в данному случае кроме конкурентности мы сможем также получить сохранение последовательности вывода в консоль значений hosts, поскольку значения i будут записаны в канал в порядке их итерации. Для ожидания завершения всех go-процедур используем другой буферизованный канал out размером равным размеру hosts. С помощью утверждения select считываем из него значения и подсчитываем количество вернувшихся значений либо ожидаем 1 мс и снова проверяем содержимое канала:
package main
import (
"fmt"
"time"
)
func main() {
hosts := []string{"ahost", "bhost", "chost"}
size := len(hosts)
ch := make(chan int, size)
out := make(chan int, size)
for i, _ := range hosts {
ch <- i
go func() {
fmt.Println(hosts[<-ch])
out <- 0
}()
}
var count int
for {
select {
case <-out:
count++
if count == size {
return
}
default:
time.Sleep(1 * time.Millisecond)
}
}
}
Теперь мы получили то, что предполагал данный пример изначально: конкурентное исполнение go-процедур с сохранением порядка запуска.
Запустить пример в песочнице play.golang.org
Читайте также:
Комментариев нет:
Отправить комментарий