Вступление
Недавно наткнулся на маленький полезный пакет и решил поделиться находкой. Для этого публикую перевод статьи, обсуждающей проблему корректного завершения goroutine извне и предлагающей решение в качестве того самого маленького пакета tomb.
Перевод статьи
Определенно одной из причин, почему людей привлекает язык Go, является первоклассный подход к параллелизму. Такие возможности как общение через каналы, легковесные процессы (goroutines) и их надлежащее планирование — не только являются родными для языка, но и интегрированны в него со вкусом.
Если вы послушаете разговоры в сообществе в течение нескольких дней, то велик шанс, что вы услышите, как кто-то с гордостью отметит принцип:
Не общайтесь с помощью разделяемой памяти, разделяйте память с помощью общения.
На эту тему есть запись в блоге, а также интерактивное упражнение (code walk).
Эта модель очень практична, и при разработке алгоритмов можно получить значительный выигрыш если подойти к задаче с этой стороны, но это уже не новость.
В своей заметке я хочу обратиться к открытому на данный момент в Go вопросу, связанному с этим подходом: завершение фоновой активности.
В качестве примера, давайте создадим специально упрощенную goroutine, которая посылает строки через канал:
type LineReader struct { Ch chan string r *bufio.Reader } func NewLineReader(r io.Reader) *LineReader { lr := &LineReader{ Ch: make(chan string), r: bufio.NewReader(r), } go lr.loop() return lr }
У структуры LineReader есть канал Ch, через который клиент может получать строчки, а также внутренний буфер r (недоступный извне), используемый для эффективного считывания этих строчек. Функция NewLineReader создает инициализированный LineReader, запускает цикл чтения и возвращает созданную структуру.
Теперь давайте посмотрим на сам цикл:
func (lr *LineReader) loop() { for { line, err := lr.r.ReadSlice('n') if err != nil { close(lr.Ch) return } lr.Ch <- string(line) } }
В цикле мы получаем строку из буфера, в случае ошибки закрываем канал и останавливаемся, иначе — передаем строку на другую сторону, возможно блокируясь на то время, пока она занимается своими делами. Это все понятно и привычно для Go-разработчика.
Но есть две детали, связанные с завершением этой логики: во-первых, теряется информация об ошибке, а во-вторых, нет никакого чистого способа прервать процедуру извне. Конечно, ошибку можно легко залогировать, но что, если мы хотим сохранить ее в базе данных, или отправить по проводам, или даже обработать ее, принимая во внимание ее природу? Возможность чистой остановки также во многих случаях ценна, например, для запуска из под test-runner’а.
Я не утверждаю, что это что-то, что сложно сделать, каким-либо способом. Я хочу сказать, что на сегодняшний день нет общепринятого подхода для обработки этих моментов простым и последовательным образом. Или, может быть, не было. Пакет tomb для Go — это мой эксперимент в попытке решить проблему.
Модель работы простая: Tomb отслеживает жива ли goroutine, умирает или мертва, а также причину смерти.
Чтобы понять эту модель, давайте посмотрим, как эта концепция применяется к примеру с LineReader. В качестве первого шага, нужно изменить процесс создания, чтобы добавить поддержку Tomb:
type LineReader struct {
Ch chan string
r *bufio.Reader
t tomb.Tomb
}
func NewLineReader(r io.Reader) *LineReader {
lr := &LineReader{
Ch: make(chan string),
r: bufio.NewReader(r),
}
go lr.loop()
return lr
}
Выглядит очень похоже. Только новое поле в структуре, даже функция создания не изменилась.
Далее, изменим функцию-цикл для поддержки отслеживания ошибок и прерывания:
func (lr *LineReader) loop() {
defer lr.t.Done()
for {
line, err := lr.r.ReadSlice('n')
if err != nil {
close(lr.Ch)
lr.t.Kill(err)
return
}
select {
case lr.Ch <- string(line):
case <-lr.t.Dying():
close(lr.Ch)
return
}
}
}
Отметим несколько интересных моментов: во-первых непосредственно перед тем, как завершится функция loop, вызывается Done, чтобы отследить завершение goroutine. Затем, ранее неиспользуемая ошибка теперь передается в метод Kill, что помечает goroutine как умирающую. Наконец, отсылка в канал была изменена таким образом, чтобы она не блокировалась в случае, если goroutine умирает по какой-либо причине.
У Tomb есть каналы Dying и Dead, возвращаемые одноименными методами, которые закрываются, когда Tomb меняет свое состояние соответствующим образом. Эти каналы позволяют организовать явную блокировку до тех пор, пока не изменится состояние, а также выборочно разблокировать выражение select в таких случаях, как показано выше.
Имея такой измененный цикл, как описано выше, легко реализовать метод Stop для запроса чистого синхронного завершения goroutine извне:
func (lr *LineReader) Stop() error {
lr.t.Kill(nil)
return lr.t.Wait()
}
В этом случае, метод Kill переведет tomb в умирающее состояние извне выполняющейся goroutine, и метод Wait приведет к блокировке до тех пор, пока goroutine не завершится и не сообщит об этом через метод Done, как было показано выше. Эта процедура ведет себя корректно даже если goroutine уже была мертва или в умирающем состоянии из-за внутренних ошибок, потому что только первый вызов метода Kill с настоящей ошибкой запоминается в качестве причины смерти goroutine. Значение nil, переданное в t.Kill используется как причина чистого завершения без фактической ошибки и приводит к тому, что Wait возвращает nil по завершении goroutine, что свидетельствует о чистой остановке по идиомам Go.
Вот собственно и все что можно сказать по теме. Когда я начал разрабатывать на Go 1, я задавался вопросом, нужна ли бо́льшая поддержка со стороны языка, чтобы придумать хорошее соглашения для такого рода проблем, как например некое отслеживание состояния самой goroutine по аналогии с тем, что делает Erlang со своими легковесными процессами, но оказалось что это больше вопрос организации рабочего процесса с использованием существующих строительных блоков.
Пакет tomb и его тип Tomb — фактическое представление хорошей договоренности по завершению goroutine, со знакомыми именами методов, вдохновленное существующими идиомами. Если вы хотите воспользоваться этим, то пакет можно получить с помощью команды:
$ go get launchpad.net/tomb
Детальная документация API доступна по адресу:
gopkgdoc.appspot.com/pkg/launchpad.net/tomb
Удачи!
Автор: Kluyg