Как стать автором
Обновить

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

Можно ли в Go расшарить состояние между двумя горутинами? Или обмениваться данными можно только строго через каналы?
Есть библиотека sync.Mutex
В Go есть и низкоуровневые примитивы синхронизации (не в той мере как… в C, POSIX, но основные). Можно пользоваться ними. Но горутины и каналы — это высокоровневый механизм… идущий от мониторов Хоара, он гораздо больше свободен от ошибок.
Если некторые структуры данных объявлены в области видимости нескольких функций-горутин, они вполне могут совместно использовать такие данные. Не зря синтаксис Go расширен (от C) вложенными опредделенияи функций многих уровней.
Вот пример без использования каналов, а на чистом sync.WaitGroup play.golang.org/p/_y6NU9tVjt
Этот подход позволяет обойтись без каналов, в некоторых случаях.
Но вот внизу, канал по прежнему останется двунаправленным.
Мне понятно что вы здесь показываете, но непонятно зачем. Зачем вам принципиально однонаправленный канал?
это была цитата из текста… но оформленная неправльно…
Всё это позволяет отловить некоторые ошибки на этапе компиляции.

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

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


выполнение главной горутины будет блокироваться на чтении из канала до тех пор, пока мы туда не запишем что-то через done < — (либо не закроем канал).
Канал в таком виде — синхронный и блокирующий.
Вероятно Вы правы — добавил поясняющие комментарии. Кстати, я там накосячил в статье. Вместо
done <- struct{} // тип
Нужно
done <- struct{}{} // экземпляр
Статью исправил.
Вообще множество аспектов касающихся каналов не освещены в статье, ибо тогда она получилась бы чересчур большой громоздкой.
Размер 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
}

Неблокирующее — это default в select. А тут скорее проверка на закрытость канала.
А вот и нет, и в первом и во втором случае у Вас создаётся канал предназначенный для конкретного значения. Даже если не пересылать ничего, а использовать закрытие канала — нет нужды делать его типа interface{} или bool. В любом случае — это какие-то значения. Вот например размеры play.golang.org, и nil — это тоже значение, в данном случае. Суть в том, что любое количество struct{} будет занимать 0 памяти, а указатель на переменную содержащую struct{}{} будет всегда один и тот же, для всех таких переменных. Иными словами — это ничего в классическом понимании. С таким же успехом, вместо inteface{} можно пулять любой референсный тип, но опять же зачем?
nil — это не нулевой указатель?
Который всегда будет занимать ровно столько же, сколько и указатель на пустую структуру? (4 или 8 байт в зависимости от архитектуры). Но по нулевому указателю сразу понятно, что он нулевой.
А указатель на struct{}{} получается еще раскрыть надо.
Или я ошибаюсь?
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 ровно как и на любой другой референсный тип.
В любом случае я не думаю, что если использовать НЕ struct{}{} — то приложение выжрет всю память, или будет жёстко тормозить. Вот есть такая фишка как struct{}{} — это основной посыл. И эта struct{}{} применима только для пересылки по каналу, когда по сути не важно, что пересылать. В коде такие пересылки будут явно означать (при просмотре), что речь не о передаче данных, а о каком-то сигнале/событии.
> runtime.LockOSThread()
> к текущему _процессу_ ОС

Я надеюсь, это опечатка?
Спасибо, исправил. О, кто-то смотрел спойлеры, а я уж думал что зря их добавил.
> При этом другие горутины спокойно могут выполняться в параллельных процессах.
Ну где ж вы процессы там увидели.

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


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

Но более того, есть публикации, которые утверждают, что горутины даже не являются потоками ядра, а являются ещ более легковесными механизмами пространства пользователя.
Но более того, есть публикации, которые утверждают, что горутины даже не являются потоками ядра, а являются ещ более легковесными механизмами пространства пользователя.
И это так. Но это не значит, что горутины не используют треды ОС.
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 не процесс — Ваша правда. Я всегда думал иначе. Странно.
Вы опять путаете. Горутины выполняются на тредах ОС как 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.
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 и не разбираться, что и где выполняется. Ведь библиотек, которые требуют выполнение строго во втором или третьем треде ОС нет.
> «юзер-спейс треды» не мешают использовать thread local storage.

В общем случае именно мешают, поскольку никто не гарантирует, что они выполняются на одном OS-треде(да и вообще, на кой они такие нужны, если на одном треде), и что они выполняются на одном и том же треде.
Можно вызывать runtime.LockOSThread в каждой горутине — и это гарантирует: горутина=OS тред.
Вот, кстати, отличный пост по теме http://morsmachine.dk/go-scheduler
Спс
Я бы на месте тех, кто интересуется Go, а Go наращивается очень динамично, несравнимо ни с одним инструметом программирования, создал бы где-то тему, где собирал бы URL всё новых и новых публикаций и книг по Go.

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

Публикации

Истории