Конкурентное программирование имеет свои идиомы. Хороший пример - таймауты. Хотя каналы Go не поддерживают их напрямую, их легко реализовать. Скажем, мы хотим получить из канала ch, но хотим подождать максимум одну секунду, пока значение не придет. Мы начнем с создания канала сигнализации и запуска goroutine, которая засыпает перед отправкой по каналу:
timeout := make(chan bool, 1)
go func() {
time.Sleep(1 * time.Second)
timeout <- true
}()
Затем мы можем использовать оператор select для получения из ch или timeout. Если ничего не приходит из ch через одну секунду, выбирается случай timeout, и попытка чтения из ch прекращается.
select {
case <-ch:
// произошло чтение из ch
case <-timeout:
// время ожидания чтения из ch истекло
}
Канал timeout буферизуется с местом для 1 значения, что позволяет timeout goroutine пересылать в канал и затем выходить. goroutine не знает (или не заботится), получено ли значение. Это означает, что goroutine не будет зависать вечно, если получение из ch произойдет до истечения времени ожидания. Канал timeout в конечном итоге будет уничтожен сборщиком мусора.
(В этом примере мы использовали time.Sleep, чтобы продемонстрировать механизм работы с goroutine'ами и каналами. В реальных программах вы должны использовать `time.After`, функцию, которая возвращает канал и отправляет по этому каналу после указанной продолжительности.)
Посмотрим на другой вариант этого шаблона. В этом примере у нас есть программа, которая читает из нескольких реплицированных баз данных одновременно. Программа нуждается только в одном из ответов, и она должна принять ответ, который прибывает первым.
Функция Query принимает часть соединений с базой данных и строку запроса. Она запрашивает каждую из баз данных параллельно и возвращает первый полученный ответ:
func Query(conns []Conn, query string) Result {
ch := make(chan Result)
for _, conn := range conns {
go func(c Conn) {
select {
case ch <- c.DoQuery(query):
default:
}
}(conn)
}
return <-ch
}
В этом примере замыкание выполняет неблокирующую отправку, что достигается с помощью операции отправки в select утверждении с default случаем. Если отправка не может быть выполнена немедленно, будет выбран default случай. Отсутствие блокировки отправки гарантирует, что ни одна из goroutine, запущенных в цикле, не будет зависать. Однако, если результат приходит до того, как основная функция добралась до получения, отправка может завершиться неудачей, поскольку никто не готов.
Эта проблема - пример того, что известно как состояние гонки, но решение тривиально. Мы просто обязательно буферизуем канал ch (добавляя длину буфера в качестве второго аргумента для make), гарантируя, что при первой отправке будет место куда положить значение. Это гарантирует, что отправка всегда будет успешной, и первое полученное значение будет получено независимо от порядка выполнения.
Эти два примера демонстрируют простоту, с которой Go может выражать сложные взаимодействия между программами.
Читайте также:
- Модель памяти Go
- Эффективный Go: параллелизм, go-процедуры (goroutines)
- Использование sync.Once для однократного вызова функций из конкурентных go-процедур
Комментариев нет:
Отправить комментарий