- Каналы и пустое значение
- Односторонние каналы
- Выполнение в основном треде ОС
- Вынос блокирующих операций
Каналы и пустое значение
Каналы — это инструмент для асинхронной разработки. Но зачастую не важно что переслать по каналу — важен лишь факт пересылки. Порой встречается
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{})При частой необходимости, можно сделать функцию, которая будет всем этим заниматься. Вот пример на play.golang.org. Всё это позволяет отловить некоторые ошибки на этапе компиляции.
writingChan := (chan<- struct{})(done) // первые скобки не важны
readingChan := (<-chan struct{})(done) // первые скобки обязательны
Выполнение в основном треде ОС
Например такие библиотеки как — OpenGL, libSDL, Cocoa — используют локальные для процесса структуры данных (thread local storage). Это значит, что они должны выполняться в основном треде ОС (main OS thread), иначе — ошибка. Функция
runtime.LockOSThread()
позволяет приморозить текущую горутину к текущему треду (thread) ОС. Если вызвать её при инициализации (в функции init
), то это и будет основной тред ОС (main OS thread). При этом другие горутины спокойно могут выполняться в параллельных тредах ОС.Для того, чтобы вынести вычисления в отдельный тред (в данном случае речь о горутине, не факт что она будет в отдельном треде ОС) достаточно просто пересылать функции в основной. Вот и всё.
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-операции, но они побеждаются аналогично.
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 // ожидание завершения
}
Референс
- Разъяснение LockOSThread (англ.)
- Пустые структуры на blog.golang.org (англ.)
- Ещё про пустые структуры (англ.)
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (32)
Olej
27.09.2015 21:36Но вот внизу, канал по прежнему останется двунаправленным.
Мне понятно что вы здесь показываете, но непонятно зачем. Зачем вам принципиально однонаправленный канал?deep_orange
27.09.2015 21:50Всё это позволяет отловить некоторые ошибки на этапе компиляции.
Только это. Каналов может быть тьма, ненароком можно и записать куда не стоит. По ссылке на play.golang.org вообще нет двунаправленного канала — один для записи, другой для чтения, вот и всё. Разумеется для простеньких задач (как эти примеры) — это слишком (а может — хорошая практика, приучать себя к этому). Например, в пакете time, такие функции как After и Tick возвращают строго канал только для чтения. Достаточно трудно намудрить так, что бы потом записать в этот канал. Но если да — то выхлоп компилятора чётко укажет на ошибку.
var-log
27.09.2015 23:56Например при реализации геттера, который возвращает приватное поле-канал (может быть полезно для использования с select). Если просто передать канал, то есть опасность, что туда могут записать то, что не нужно. Могу кинуть реальный пример, сегодня пришлось писать свою реализацию WaitGroup из-за того, что тот нельзя использовать с select для ожидания.
isden
28.09.2015 10:14Еще, имхо, весьма важный момент, о котором стоит сказать явно.
В коде вида
func main() { done := make(chan struct{}) go func() { // stuff done <- struct{} }() <- done }
выполнение главной горутины будет блокироваться на чтении из канала до тех пор, пока мы туда не запишем что-то через done < — (либо не закроем канал).
Канал в таком виде — синхронный и блокирующий.deep_orange
28.09.2015 13:59Вероятно Вы правы — добавил поясняющие комментарии. Кстати, я там накосячил в статье. Вместо
Нужноdone <- struct{} // тип
Статью исправил.done <- struct{}{} // экземпляр
Вообще множество аспектов касающихся каналов не освещены в статье, ибо тогда она получилась бы чересчур большой громоздкой.
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 }
AterCattus
28.09.2015 12:36Неблокирующее — это default в select. А тут скорее проверка на закрытость канала.
AterCattus
28.09.2015 12:47Приведенный вариант play.golang.org/p/2QzOFUWuTW: «fatal error: all goroutines are asleep — deadlock!»
Вариант с default play.golang.org/p/GnXhNddvgZ: работает.
deep_orange
28.09.2015 13:33+2А вот и нет, и в первом и во втором случае у Вас создаётся канал предназначенный для конкретного значения. Даже если не пересылать ничего, а использовать закрытие канала — нет нужды делать его типа
interface{}
илиbool
. В любом случае — это какие-то значения. Вот например размеры play.golang.org, иnil
— это тоже значение, в данном случае. Суть в том, что любое количествоstruct{}
будет занимать 0 памяти, а указатель на переменную содержащуюstruct{}{}
будет всегда один и тот же, для всех таких переменных. Иными словами — это ничего в классическом понимании. С таким же успехом, вместоinteface{}
можно пулять любой референсный тип, но опять же зачем?el777
29.09.2015 11:03nil — это не нулевой указатель?
Который всегда будет занимать ровно столько же, сколько и указатель на пустую структуру? (4 или 8 байт в зависимости от архитектуры). Но по нулевому указателю сразу понятно, что он нулевой.
А указатель на struct{}{} получается еще раскрыть надо.
Или я ошибаюсь?deep_orange
29.09.2015 14:25nil
— это нулевой указатель, всё верно. И он занимаетuintptr
места. Указатель наstruct{}{}
будет занимать столько же места, столько же места будет занимать[]byte
или&struct{ A, B, C int }
. Я не призываю использовать указатель наstruct{}{}
, ибо это вряд ли когда пригодиться. Но хочу подчеркнуть — чтоstruct{}{}
— всегда один и тот же экземпляр. Вот например
На play.golang.org с раскрытием значенияvar a, b struct{} a == b // true &a == &b // true
&struct{}{}
.
interface{}
, кстати — это что-то вроде указателя на структуру вида struct{ typePtr, valPrt }. Вот референс (англ.). Разумеется на несуществующийinterface{}
будет указыватьnil
ровно как и на любой другой референсный тип.
deep_orange
29.09.2015 14:48В любом случае я не думаю, что если использовать НЕ
struct{}{}
— то приложение выжрет всю память, или будет жёстко тормозить. Вот есть такая фишка какstruct{}{}
— это основной посыл. И этаstruct{}{}
применима только для пересылки по каналу, когда по сути не важно, что пересылать. В коде такие пересылки будут явно означать (при просмотре), что речь не о передаче данных, а о каком-то сигнале/событии.
nwalker
28.09.2015 17:19+1> runtime.LockOSThread()
> к текущему _процессу_ ОС
Я надеюсь, это опечатка?deep_orange
28.09.2015 17:46Спасибо, исправил. О, кто-то смотрел спойлеры, а я уж думал что зря их добавил.
nwalker
28.09.2015 17:59+1> При этом другие горутины спокойно могут выполняться в параллельных процессах.
Ну где ж вы процессы там увидели.
Ну и да, эту функцию можно использовать исключительно понимая, что делаешь.deep_orange
28.09.2015 18:17Всё время мысленно провожу аналогию с POSIX threads — а это отдельные процессы (по крайней мере в Linux). Отсюда и такие досадные ошибки. Я хотел подчеркнуть, что
LockOSThread
не заставляет всю программу выполняться в одном треде, а только текущую горутину. Ща исправлю, на более корректное высказывание. Спасибо, Вы очень внимательны.Olej
28.09.2015 18:32Всё время мысленно провожу аналогию с POSIX threads — а это отдельные процессы (по крайней мере в Linux).
Да ничего подобного! Оттого, что pthread_t создаются тем же вызовом clone(), что и процессы, они не становятся процессами.
Но более того, есть публикации, которые утверждают, что горутины даже не являются потоками ядра, а являются ещ более легковесными механизмами пространства пользователя.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 не процесс — Ваша правда. Я всегда думал иначе. Странно.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.deep_orange
29.09.2015 02:02Go 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 и не разбираться, что и где выполняется. Ведь библиотек, которые требуют выполнение строго во втором или третьем треде ОС нет.nwalker
29.09.2015 19:27> «юзер-спейс треды» не мешают использовать thread local storage.
В общем случае именно мешают, поскольку никто не гарантирует, что они выполняются на одном OS-треде(да и вообще, на кой они такие нужны, если на одном треде), и что они выполняются на одном и том же треде.deep_orange
01.10.2015 22:21Можно вызывать
runtime.LockOSThread
в каждой горутине — и это гарантирует: горутина=OS тред.
nwalker
29.09.2015 00:45+1Вот, кстати, отличный пост по теме http://morsmachine.dk/go-scheduler
Olej
29.09.2015 09:42Я бы на месте тех, кто интересуется Go, а Go наращивается очень динамично, несравнимо ни с одним инструметом программирования, создал бы где-то тему, где собирал бы URL всё новых и новых публикаций и книг по Go.
Вот, кстати, очень активный руссоязычный ресурс Язык программирования Go — на нём чуть ли не ежедневно выкладываются свежие статьи, переводы, ссылки.
nehaev
Можно ли в Go расшарить состояние между двумя горутинами? Или обмениваться данными можно только строго через каналы?
kolesnevg
Есть библиотека sync.Mutex
Olej
В Go есть и низкоуровневые примитивы синхронизации (не в той мере как… в C, POSIX, но основные). Можно пользоваться ними. Но горутины и каналы — это высокоровневый механизм… идущий от мониторов Хоара, он гораздо больше свободен от ошибок.
Если некторые структуры данных объявлены в области видимости нескольких функций-горутин, они вполне могут совместно использовать такие данные. Не зря синтаксис Go расширен (от C) вложенными опредделенияи функций многих уровней.
deep_orange
Вот пример без использования каналов, а на чистом sync.WaitGroup play.golang.org/p/_y6NU9tVjt
Этот подход позволяет обойтись без каналов, в некоторых случаях.