пятница, 22 ноября 2019 г.

Golang puzzlers: синхронизации go-процедур (goroutine)

В этом посте рассмотрим задачу синхронизации 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


Читайте также:


Комментариев нет:

Отправить комментарий