Go, практика асинхронного взаимодействия
Немножко про каналы, про выполнение в основном процессе, про то как вынести блокирующие операции в отдельную горутину.
- Каналы и пустое значение
- Односторонние каналы
- Выполнение в основном процессе
- Вынос блокирующих операций
Каналы и пустое значение
Каналы — это инструмент для асинхронной разработки. Но зачастую не важно что переслать по каналу — важен лишь факт пересылки. Порой встречается
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 thread) ОС, иначе — ошибка. Функция runtime.LockOSThread()
позволяет приморозить текущую горутину к текущему процессу ОС. Если вызвать её при инициализации (в функции init
), то это и будет основной процесс ОС. При этом другие горутины спокойно могут выполняться в параллельных процессах.
Для того, чтобы вынести вычисления в отдельный процесс достаточно просто пересылать функции в основной. Вот и всё.
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 // ожидание завершения
}
Если несколько горутин будут отправлять свои сообщения к одной «пишущей», то они всё равно будут блокироваться. В этом случае выручит канал с буфером. Учитывая, что slice — это референсный тип, по каналу будет пересылаться только указатель.
Референс
- Разъяснение LockOSThread (англ.)
- Пустые структуры на blog.golang.org (англ.)
- Ещё про пустые структуры (англ.)
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.