среда, 30 января 2019 г.

Эффективный Go: параллелизм, go-процедуры (goroutines)

Разделение через общение

Параллельное программирование - это большая тема, и здесь будут упомянуты только некоторые Go-специфичные моменты.

Параллельное программирование во многих средах затруднено ввиду тонкостей и особенностей, необходимых для правильного доступа к разделяемым переменным. Go поощряет другой подход, в котором общие значения передаются по каналам и, фактически, никогда активно не разделяется отдельными потоками исполнения. Только одна программа имеет доступ к значению в любой момент времени. Гонки данных не могут происходить, по замыслу. Для поощрения такого мышления существует лозунг:

Не общайтесь, разделяя память; вместо этого делитесь памятью, общаясь.

Этот подход может быть принят слишком далеко. Подсчет ссылок может быть лучше всего осуществлен, например, помещением мьютекса вокруг целочисленной переменной. Но как подход высокого уровня, использование каналов для контроля доступа облегчает написание четких, правильных программ.

Один из способов обдумать эту модель - рассмотреть типичную однопоточную программу, которая работает на одном процессоре. Она не нуждается в синхронизации примитивов. Теперь запустите другой такой экземпляр; он тоже не нуждается в синхронизации. Теперь пусть эти экземпляры общаются; если связь является синхронизатором, по-прежнему нет необходимости для другой синхронизации. Unix-конвейеры (Unix pipelines), например, подходят для этой модели в совершенстве. Хотя подход Go к параллелизму берет свое начало в Связи последовательных процессов Хора (Hoare's Communicating Sequential Processes) (CSP), его также можно рассматривать как безопасное для типов обобщение каналов Unix.

Go-процедуры (Goroutines)

Они называются Go-процедуры (goroutines), потому что существующие термины - потоки (threads), сопрограммы (coroutines), процессы и т. д. - передают неточные значения. Go-процедура (goroutine) имеет простую модель: это функция, выполняющаяся одновременно с другими go-процедурами в том же адресном пространстве. Она легковесная, стоящая чуть больше, чем аллокация стекового пространства. И стеки стартуют малыми, поэтому они дешевы и растут выделяя (и освобождая) хранилище heap по мере необходимости.

Go-процедуры мультиплексируются в несколько потоков ОС, так что если одна из них нуждается в блокировке, например во время ожидания ввода-вывода, другие продолжают работать. Их дизайн скрывает многие сложности создания потоков и управления ими.

Используйте префикс с ключевым словом go для вызова функции или метода для запуска вызова в новой Go-процедуре. Когда вызов завершается, Go-процедура выходит молча. (Эффект похож на нотацию & в оболочке Unix для запуска команды в фоне.)

// выполняем list.Sort параллельно; не ждите его.
go list.Sort()  

Литерал функции может быть полезен при вызове go-процедуры.

func Announce(message string, delay time.Duration) {
    go func() {
        time.Sleep(delay)
        fmt.Println(message)
    }()  
    // Обратите внимание на круглые скобки 
    // - должна быть вызвана функция.
}

В Go литералы функций являются замыканиями: реализация убеждается, что переменные, на которые ссылается функция, сохраняются, пока они активны.

Эти примеры не слишком практичны, потому что функции не могут сигнализировать о завершении. Для этого нам нужны каналы, которые мы рассмотрим в следующем посте.


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


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

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