Показаны сообщения с ярлыком Golang puzzlers. Показать все сообщения
Показаны сообщения с ярлыком Golang puzzlers. Показать все сообщения

пятница, 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


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


четверг, 21 ноября 2019 г.

Golang puzzlers: длина строки

Пример этого поста: определение длины строки. Дан код:

package main

import "fmt"

func main() {
  test := "frйday"
  fmt.Println(len(test))
}

Вопрос: что выведется при запуске?

На первый взгляд покажется что ответ должен быть 6, поскольку в строке 6 символов. Но при запуске в выводе будет 7!

В чем подвох? Дело в том что len измеряет строку по байтам, а не по символам. 5 символов frday являются ASCII символами и в UTF-8 (а исходный код всех строк в Go представлен UTF-8 текстом) каждый из этих символов занимает 1 байт, а символ й не является ASCII символом, и в UTF-8 занимает 2 байта, поэтому 5+2=7. Стоит сразу отметить, что исходный код строк в Go состоит из UTF-8 текста, но в UTF-8 если символ вмещается в 1 байт (как с ASCII символами), то он и записывается 1 байтом. Символ й занимает 2 байта, но если бы мы взяли какой-нибудь китайский иероглиф, который в UTF-8 занимает 3 байта, в нашем примере, то могли бы получить даже 8 в ответе, например:

package main

import "fmt"

func main() {
  test := "fr語day"
  fmt.Println(len(test))
}

Вывод:

8

Запустить пример в песочнице play.golang.org

Для более подробной информации о строках, символах и кодировке читайте этот пост.


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


Golang puzzlers: возврат ошибки

В постах Golang puzzlers мы рассмотрим различные случаи в Golang, которые могут быть неочевидными на первый взгляд.

Пример этого поста. Дан код:

package main

import (
 "fmt"
)

type myType struct {
 msg string
}

func (m *myType) Error() string {
 return m.msg
}

func test() *myType {
 return nil
}

func main() {
 var err error
 err = test()
 if err != nil {
  fmt.Println("occured error")
  return
 }
 fmt.Println("ok")
}

Вопрос: что выведет данный код?

На первый взгляд все очевидно: задан тип myType, указатель на который реализует интерфейс error. Раз указатель на myType реализует интерфейс ошибки, то кажется что все верно - функция test возвращает указатель на myType, а в функции main возвращаемое значение будет записано в переменную типа error. При запуске программа скомпилируется и кажется, что раз мы возвращаем nil в test, то будет выведено ok, но в консоль выведется occured error!

Начнем разбор - распечатаем переменную err, которая должна быть равна nil:

package main

import (
 "fmt"
)

type myType struct {
 msg string
}

func (m *myType) Error() string {
 return m.msg
}

func test() *myType {
 return nil
}

func main() {
 var err error
 err = test()
 if err != nil {
  fmt.Println("occured error")
  fmt.Println(err)
  return
 }
 fmt.Println("ok")
}

При запуске получим:

occured error
<nil>

Значит err равно nil? В чем подвох? Мы сможем понять это использовав пакет reflect для разбора:

package main

import (
 "fmt"
 "reflect"
)

type myType struct {
 msg string
}

func (m *myType) Error() string {
 return m.msg
}

func test() *myType {
 return nil
}

func main() {
 var err error
 err = test()
 if err != nil {
  fmt.Println("error")
  fmt.Println(err)
  e := reflect.ValueOf(err)
  fmt.Println(e)
  fmt.Println(e.Kind())
  return
 }
 fmt.Println("ok")
 fmt.Println(err)
}

При запуске получим:

error
<nil>
<nil>
ptr

Что получается? В err записывается указатель со значением nil. Это указатель на тип myType, который реализует интерфейс error, поэтому этот указатель можно записать в переменную типа error - компилятор позволит нам это. Но при проверке на nil за nil засчитается только значение записанное по типу интерфейса error - то есть если бы функция test в качестве возвращаемого значения в сигнатуре функции указывала бы интерфейс error, тогда все бы работало как предполагается и в консоль было бы выведено ok.

package main

import (
 "fmt"
)

type myType struct {
 msg string
}

func (m *myType) Error() string {
 return m.msg
}

func test() error {
 return nil
}

func main() {
 var err error
 err = test()
 if err != nil {
  fmt.Println("error")
  return
 }
 fmt.Println("ok")
}

При запуске получим:

ok

Запустить пример в песочнице play.golang.org


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