четверг, 17 октября 2019 г.

Паттерны конкурентности в Golang: таймаут, движение дальше

Конкурентное программирование имеет свои идиомы. Хороший пример - таймауты. Хотя каналы 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 может выражать сложные взаимодействия между программами.


Читайте также:


Комментариев нет:

Отправить комментарий