Немножко про каналы, про выполнение в основном процессе, про то как вынести блокирующие операции в отдельную горутину.
  • Каналы и пустое значение
  • Односторонние каналы
  • Выполнение в основном треде ОС
  • Вынос блокирующих операций



Каналы и пустое значение


Каналы — это инструмент для асинхронной разработки. Но зачастую не важно что переслать по каналу — важен лишь факт пересылки. Порой встречается
done := make(chan bool)
/// [...]
done <- true

Размер bool зависит от платформы, да, обычно, это не тот случай, когда следует беспокоиться о размере. Но всё же существует способ ничего не отправлять, а если точнее — то отправлять ничего (если быть ещё точнее, то речь о пустой структуре).
done := make(chan struct{})
// [...]
done <- struct{}{}

Вот собственно и всё.

Односторонние каналы


Есть ещё один момент, который хотелось бы явно осветить. Пример:
func main() {
    done := make(chan struct{})
    go func() {
        // stuff
        done <- struct{}{} // перед завершением сообщаем об этом
    }()
    <- done // ожидание завершения горутины
}

Всё просто — done в горутине нужен только для записи. В принципе, в горутине его можно и прочитать (получить значение из канала done). Во избежании неприятностей, если код путаный, выручают параметры. Параметры функции, что передаётся горутине. Теперь так
func main() {
    done := make(chan struct{})
    go func(done chan<- struct{}) {
        // stuff
        done <- struct{}{} // перед завершением сообщаем об этом
    } (done)
    <- done // ожидание завершения горутины
}
Теперь, при передаче канала так, он будет преобразован в канал только для записи. Но вот внизу, канал по прежнему останется двунаправленным. В принципе, канал можно преобразовать в односторонний и не передавая его аргументом:
done := make(chan struct{})
writingChan := (chan<- struct{})(done) // первые скобки не важны
readingChan := (<-chan struct{})(done) // первые скобки обязательны
При частой необходимости, можно сделать функцию, которая будет всем этим заниматься. Вот пример на play.golang.org. Всё это позволяет отловить некоторые ошибки на этапе компиляции.

Выполнение в основном треде ОС


Например такие библиотеки как — OpenGL, libSDL, Cocoa — используют локальные для процесса структуры данных (thread local storage). Это значит, что они должны выполняться в основном треде ОС (main OS thread), иначе — ошибка. Функция runtime.LockOSThread() позволяет приморозить текущую горутину к текущему треду (thread) ОС. Если вызвать её при инициализации (в функции init), то это и будет основной тред ОС (main OS thread). При этом другие горутины спокойно могут выполняться в параллельных тредах ОС.

Для того, чтобы вынести вычисления в отдельный тред (в данном случае речь о горутине, не факт что она будет в отдельном треде ОС) достаточно просто пересылать функции в основной. Вот и всё.
Простыня
На play.golang.org
package main
 
import (
        "fmt"
        "runtime"
)
 
func init() {
        runtime.LockOSThread() // примораживаем текущую горутину к текущему треду
}
 
func main() {
        /*
            коммуникации
        */

        done := make(chan struct{})    // <- остановка и выход
        stuff := make(chan func()) // <- отправка функций в основной тред
 
        /*
            создадим второй тред (в данном случае - вторую горутину, но  это не важно)
            и начнём отправлять "работу" в первый
        */

        go func(done chan<- struct{}, stuff chan<- func()) { // параллельная работа
                stuff <- func() { // первый пошёл
                        fmt.Println("1")
                }
                stuff <- func() { // второй пошёл
                        fmt.Println("2")
                }
                stuff <- func() { // третий пошёл
                        fmt.Println("3")
                }
                done <- struct{}{}
        }(done, stuff)
Loop:
        for {
                select {
                case do := <-stuff: // получение "работы"
                        do()        // и выполнение
                case <-done:
                        break Loop
                }
        }
}



Вынос блокирующих операций


Куда чаще встречаются блокирующие IO-операции, но они побеждаются аналогично.
Простыня
На play.golang.org
package main
 
import "os"
 
func main() {
        /*
                коммуникации
        */

        stop := make(chan struct{}) // нужен для остановки "пишущей" горутины
        done := make(chan struct{}) // ожидание её завершения
        write := make(chan []byte) // данные для записи
 
        /*
                параллельный поток для IO-операций
        */

        go func(write <-chan []byte, stop <-chan struct{}, done chan<- struct{}) {
        Loop:
                for {
                        select {
                        case msg := <-write: // получения сообщения для записи
                                os.Stdout.Write(msg) // асинхронная запись
                        case <-stop:
                                break Loop
                        }
                }
                done <- struct{}{}
        }(write, stop, done)
        write <- []byte("Hello ")    // отправка сообщений
        write <- []byte("World!\n")  // на запись
        stop <- struct{}{} // остановка
        <-done // ожидание завершения
}

Если несколько горутин будут отправлять свои сообщения к одной «пишущей», то они всё равно будут блокироваться. В этом случае выручит канал с буфером. Учитывая, что slice — это референсный тип, по каналу будет пересылаться только указатель.




Референс



  1. Разъяснение LockOSThread (англ.)
  2. Пустые структуры на blog.golang.org (англ.)
  3. Ещё про пустые структуры (англ.)
Материал

Проголосовало 163 человека. Воздержалось 67 человек.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Комментарии (32)


  1. nehaev
    27.09.2015 20:41

    Можно ли в Go расшарить состояние между двумя горутинами? Или обмениваться данными можно только строго через каналы?


    1. kolesnevg
      27.09.2015 21:38
      +2

      Есть библиотека sync.Mutex


    1. Olej
      27.09.2015 21:40

      В Go есть и низкоуровневые примитивы синхронизации (не в той мере как… в C, POSIX, но основные). Можно пользоваться ними. Но горутины и каналы — это высокоровневый механизм… идущий от мониторов Хоара, он гораздо больше свободен от ошибок.
      Если некторые структуры данных объявлены в области видимости нескольких функций-горутин, они вполне могут совместно использовать такие данные. Не зря синтаксис Go расширен (от C) вложенными опредделенияи функций многих уровней.


    1. deep_orange
      28.09.2015 20:30

      Вот пример без использования каналов, а на чистом sync.WaitGroup play.golang.org/p/_y6NU9tVjt
      Этот подход позволяет обойтись без каналов, в некоторых случаях.


  1. Olej
    27.09.2015 21:36

    Но вот внизу, канал по прежнему останется двунаправленным.
    Мне понятно что вы здесь показываете, но непонятно зачем. Зачем вам принципиально однонаправленный канал?


    1. Olej
      27.09.2015 21:44

      это была цитата из текста… но оформленная неправльно…


    1. deep_orange
      27.09.2015 21:50

      Всё это позволяет отловить некоторые ошибки на этапе компиляции.

      Только это. Каналов может быть тьма, ненароком можно и записать куда не стоит. По ссылке на play.golang.org вообще нет двунаправленного канала — один для записи, другой для чтения, вот и всё. Разумеется для простеньких задач (как эти примеры) — это слишком (а может — хорошая практика, приучать себя к этому). Например, в пакете time, такие функции как After и Tick возвращают строго канал только для чтения. Достаточно трудно намудрить так, что бы потом записать в этот канал. Но если да — то выхлоп компилятора чётко укажет на ошибку.


    1. var-log
      27.09.2015 23:56

      Например при реализации геттера, который возвращает приватное поле-канал (может быть полезно для использования с select). Если просто передать канал, то есть опасность, что туда могут записать то, что не нужно. Могу кинуть реальный пример, сегодня пришлось писать свою реализацию WaitGroup из-за того, что тот нельзя использовать с select для ожидания.


  1. isden
    28.09.2015 10:14

    Еще, имхо, весьма важный момент, о котором стоит сказать явно.
    В коде вида

    func main() {
        done := make(chan struct{})
        go func() {
            // stuff
            done <- struct{}
        }()
        <- done
    }
    


    выполнение главной горутины будет блокироваться на чтении из канала до тех пор, пока мы туда не запишем что-то через done < — (либо не закроем канал).
    Канал в таком виде — синхронный и блокирующий.


    1. deep_orange
      28.09.2015 13:59

      Вероятно Вы правы — добавил поясняющие комментарии. Кстати, я там накосячил в статье. Вместо

      done <- struct{} // тип
      
      Нужно
      done <- struct{}{} // экземпляр
      
      Статью исправил.
      Вообще множество аспектов касающихся каналов не освещены в статье, ибо тогда она получилась бы чересчур большой громоздкой.


  1. utrack
    28.09.2015 10:21

    Размер bool зависит от платформы, да, обычно, это не тот случай, когда следует беспокоиться о размере. Но всё же существует способ ничего не отправлять, а если точнее — то отправлять ничего (если быть ещё точнее, то речь о пустой структуре).

    done := make(chan struct{})
    // [...]
    done < — struct{}{}


    Можно сделать ещё «легче»:
        done := make(chan interface{})
        //
        done <- nil
    


    Или не посылать ничего:
     done := make(chan interface{})
        //
        close(done)
        // в родителе
        select {
          case _,ok := <- done:
          if !ok {
            // канал закрыт
          }
        }
    


    А для неблокирующего чтения также можно использовать select:
    ch := make(chan int)
    // ...
    select {
      val, ok := <- ch:
        // обработка значения
        // если канал пуст - управление возвращается коду
        // если канал закрыт - val == nil, ok == false
    }
    


    1. AterCattus
      28.09.2015 12:36

      Неблокирующее — это default в select. А тут скорее проверка на закрытость канала.


      1. AterCattus
        28.09.2015 12:47

        Приведенный вариант play.golang.org/p/2QzOFUWuTW: «fatal error: all goroutines are asleep — deadlock!»
        Вариант с default play.golang.org/p/GnXhNddvgZ: работает.


        1. utrack
          28.09.2015 14:18

          Опечатка, да. Спасибо)


    1. deep_orange
      28.09.2015 13:33
      +2

      А вот и нет, и в первом и во втором случае у Вас создаётся канал предназначенный для конкретного значения. Даже если не пересылать ничего, а использовать закрытие канала — нет нужды делать его типа interface{} или bool. В любом случае — это какие-то значения. Вот например размеры play.golang.org, и nil — это тоже значение, в данном случае. Суть в том, что любое количество struct{} будет занимать 0 памяти, а указатель на переменную содержащую struct{}{} будет всегда один и тот же, для всех таких переменных. Иными словами — это ничего в классическом понимании. С таким же успехом, вместо inteface{} можно пулять любой референсный тип, но опять же зачем?


      1. el777
        29.09.2015 11:03

        nil — это не нулевой указатель?
        Который всегда будет занимать ровно столько же, сколько и указатель на пустую структуру? (4 или 8 байт в зависимости от архитектуры). Но по нулевому указателю сразу понятно, что он нулевой.
        А указатель на struct{}{} получается еще раскрыть надо.
        Или я ошибаюсь?


        1. deep_orange
          29.09.2015 14:25

          nil — это нулевой указатель, всё верно. И он занимает uintptr места. Указатель на struct{}{} будет занимать столько же места, столько же места будет занимать []byte или &struct{ A, B, C int }. Я не призываю использовать указатель на struct{}{}, ибо это вряд ли когда пригодиться. Но хочу подчеркнуть — что struct{}{} — всегда один и тот же экземпляр. Вот например

          var a, b struct{}
          a == b // true
          &a == &b // true
          
          На play.golang.org с раскрытием значения &struct{}{}.
          interface{}, кстати — это что-то вроде указателя на структуру вида struct{ typePtr, valPrt }. Вот референс (англ.). Разумеется на несуществующий interface{} будет указывать nil ровно как и на любой другой референсный тип.


        1. deep_orange
          29.09.2015 14:48

          В любом случае я не думаю, что если использовать НЕ struct{}{} — то приложение выжрет всю память, или будет жёстко тормозить. Вот есть такая фишка как struct{}{} — это основной посыл. И эта struct{}{} применима только для пересылки по каналу, когда по сути не важно, что пересылать. В коде такие пересылки будут явно означать (при просмотре), что речь не о передаче данных, а о каком-то сигнале/событии.


  1. nwalker
    28.09.2015 17:19
    +1

    > runtime.LockOSThread()
    > к текущему _процессу_ ОС

    Я надеюсь, это опечатка?


    1. deep_orange
      28.09.2015 17:46

      Спасибо, исправил. О, кто-то смотрел спойлеры, а я уж думал что зря их добавил.


      1. nwalker
        28.09.2015 17:59
        +1

        > При этом другие горутины спокойно могут выполняться в параллельных процессах.
        Ну где ж вы процессы там увидели.

        Ну и да, эту функцию можно использовать исключительно понимая, что делаешь.


        1. deep_orange
          28.09.2015 18:17

          Всё время мысленно провожу аналогию с POSIX threads — а это отдельные процессы (по крайней мере в Linux). Отсюда и такие досадные ошибки. Я хотел подчеркнуть, что LockOSThread не заставляет всю программу выполняться в одном треде, а только текущую горутину. Ща исправлю, на более корректное высказывание. Спасибо, Вы очень внимательны.


          1. nwalker
            28.09.2015 18:25

            BTW, в Linux уже 10 лет как нормальные треды.


          1. Olej
            28.09.2015 18:32

            Всё время мысленно провожу аналогию с POSIX threads — а это отдельные процессы (по крайней мере в Linux).


            Да ничего подобного! Оттого, что pthread_t создаются тем же вызовом clone(), что и процессы, они не становятся процессами.

            Но более того, есть публикации, которые утверждают, что горутины даже не являются потоками ядра, а являются ещ более легковесными механизмами пространства пользователя.


            1. deep_orange
              28.09.2015 19:06

              Но более того, есть публикации, которые утверждают, что горутины даже не являются потоками ядра, а являются ещ более легковесными механизмами пространства пользователя.
              И это так. Но это не значит, что горутины не используют треды ОС.
              If a goroutine is blocking, the runtime will start a new OS thread to handle the other goroutines until the blocking one stops blocking.
              Например для
              package main
              
              import "time"
              
              func comm() (chan<- struct{}, <-chan struct{}) {
                      c := make(chan struct{})
                      return c, c
              }
              
              func main() {
                      in, out := comm()
                      go func(done chan<- struct{}) {
                              time.Sleep(30*time.Second)
                              done <- struct{}{}
                      }(in)
                      time.Sleep(30*time.Second)
                      <-out
              }
              

              ps -o nlwp $PID будет 4.

              Примечание: для go < 1.5 могут быть отличия. Для GOMAXPROCS=1 число станет 3.

              Ну ладно, один тред забирает сборщик мусора. Один, может, ещё для чего или просто про-запас. В итоге получается — горутина = тред. Но, только при условии блокирующих операций в горутинах. Вот и всё.

              Про то что POSIX thread не процесс — Ваша правда. Я всегда думал иначе. Странно.


              1. nwalker
                29.09.2015 00:39

                Вы опять путаете. Горутины выполняются на тредах ОС как N:M. В go 1.5 стартовое количество тредов, GOMAXPROCS равно количеству логических ядер в системе. В вашем примере на моем рабочем компе 8 тредов, по количеству ядер. В go 1.3.3 тредов 4, чем это обусловлено мне сейчас все же некогда разбираться.

                Блокировка горутины не обязательно вызывает блокировку треда: network I/O, time.Sleep(), ожидание на канале и ожидание на примитивах из sync не вызывают блокировки треда, а только снимают горутину с выполнения до какого-то события.

                Блокировка треда совпадает с блокировкой горутины при выполнении системного вызова(syscall). Например, все file I/O, включая, вроде бы, консоль. Так же тред условно блокируется уже упомянутой runtime.LockOSThread(), на нем выполняется только горутина вызвавшая LockOSThread() и никакие другие до завершения этой горутины.
                Я сейчас уже не вдамся в подробности, когда именно создается новый тред вместо заблокированного, но в какой-то момент создается точно.

                Кстати, все вызовы через cgo считаются syscall-ами и в неблагоприятных условиях могут жрать треды только так. Если глянуть в https://github.com/golang/go/blob/master/src/runtime/cgocall.go#L86, желание использовать cgo из более чем одной-двух горутин полностью исчезнет.

                Go Scheduler Design Doc — https://golang.org/s/go11sched
                Исходники рантайма тоже рекомендуются к чтению.

                Кстати, плевок в сторону подхода авторов Go — почти ничего из написанного выше не описано в официальной документации, в т.ч. и откровенно неприятные особенности cgo.


                1. deep_orange
                  29.09.2015 02:02

                  Go 1.5 сразу формирует столько тредов ОС, сколько ядер у процессора. У ранних версий со старта выделялся только один. Вот golang.org/doc/go1.5#introduction третий пункт. Одна строчка. Но их количество можно ограничить переменной окружения GOMAXPROCS или функцией runtime.GOMAXPROCS()

                  Если вы запустите пример выше c GOMAXPROCS=1 то тредов ОС будет три. Не два, не один, не четыре и не восемь, а три. При этом, приложению нужно максимум два, учитывая, что time.Sleep() не блокирует тред — то один.

                  Да, Вы правы, вместо time.Sleep следовало использовать, например

                  buf := make([]byte, 1024)
                   _, _ = syscall.Read(0, buf) // чтение STDIN
                  


                  Но это всё не важно — ибо «юзер-спейс треды» не мешают использовать thread local storage. И сколько бы их не было — если все они в основном треде ОС, то из любого из них можно вызывать функции того же OpenGL. Это касается и горутин, но опять же — проще LockOSThread и не разбираться, что и где выполняется. Ведь библиотек, которые требуют выполнение строго во втором или третьем треде ОС нет.


                  1. nwalker
                    29.09.2015 19:27

                    > «юзер-спейс треды» не мешают использовать thread local storage.

                    В общем случае именно мешают, поскольку никто не гарантирует, что они выполняются на одном OS-треде(да и вообще, на кой они такие нужны, если на одном треде), и что они выполняются на одном и том же треде.


                    1. deep_orange
                      01.10.2015 22:21

                      Можно вызывать runtime.LockOSThread в каждой горутине — и это гарантирует: горутина=OS тред.


              1. nwalker
                29.09.2015 00:45
                +1

                Вот, кстати, отличный пост по теме http://morsmachine.dk/go-scheduler


                1. deep_orange
                  29.09.2015 02:03

                  Спс


                1. Olej
                  29.09.2015 09:42

                  Я бы на месте тех, кто интересуется Go, а Go наращивается очень динамично, несравнимо ни с одним инструметом программирования, создал бы где-то тему, где собирал бы URL всё новых и новых публикаций и книг по Go.

                  Вот, кстати, очень активный руссоязычный ресурс Язык программирования Go — на нём чуть ли не ежедневно выкладываются свежие статьи, переводы, ссылки.