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), то это и будет основной процесс ОС. При этом другие горутины спокойно могут выполняться в параллельных процессах.

Для того, чтобы вынести вычисления в отдельный процесс достаточно просто пересылать функции в основной. Вот и всё.

Простыня
На 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. Ещё про пустые структуры (англ.)

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

© Habrahabr.ru