Pull to refresh

Go, практика асинхронного взаимодействия

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



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


Каналы — это инструмент для асинхронной разработки. Но зачастую не важно что переслать по каналу — важен лишь факт пересылки. Порой встречается
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. Ещё про пустые структуры (англ.)
Only registered users can participate in poll. Log in, please.
Материал
20.98% ничего нового 43
22.44% 50/50 46
56.59% норм 116
205 users voted. 73 users abstained.
Tags:
Hubs:
+15
Comments 32
Comments Comments 32

Articles