Выводы из этого поста
- Храните в контекстах только примитивы и неизменяемые или доступные только для чтения значения
- Не используйте Context для хранения значений логики приложения
- Всегда создавайте основной отменяемый Контекст, чтобы позволить горутинам изящно останавливаться
- Проверяйте отмену Контекста до и во время выполнения длительных задач
- Используйте каналы для логического разделения одновременной работы
- Не общайтесь, разделяя память; вместо этого обменивайтесь памятью, обмениваясь данными.
Контекст
Контекст.Context — это ссылка (поскольку это интерфейс) на неизменяемую пару ключ/значение, которая является одним узлом в дереве Контекстов. Каждый Контекст может искать значения с помощью рекурсивной функции Value(key) any
и проверять отмену цепочки с помощью Done() <-chan struct{}
. Эта структура позволяет разделять значения и сигналы отмены между многочисленными горутинами. Это позволяет безопасно обмениваться данными. Одним из основных примеров использования является профилирование.
Распространенные ловушки контекста
Контекстом часто злоупотребляют и неправильно используют, позволяя значениям, хранящимся в контексте, быть изменяемыми, а также храня контексты внутри структур. Обе эти ошибки приводят к потенциальным условиям гонки и другим проблемам. Избегайте хранения карт, срезов, ссылок и каналов в качестве значений Context. Придерживайтесь примитивов и значений struct. Остерегайтесь ссылок, хранящихся в значениях struct, так как они также могут вызвать проблемы с состоянием гонки.
Вывод: Храните в Контекстах только примитивы и неизменяемые или доступные только для чтения значения.
Другой проблемой является тенденция использовать Context как мешок для хранения значений приложения. Помните, что чем больше значений хранится в Context, тем дольше становится поиск, O(n)
. Эта схема также затрудняет отладку, поскольку все значения «скрыты». Передавайте нужные вам значения в качестве аргументов.
Вывод: Не используйте Context для хранения значений логики приложения.
Хорошая практика заключается в том, чтобы всегда создавать прикладной Context перед выполнением всего остального. Это позволит main изящно завершить все запущенные goroutines перед выходом. В противном случае программы будут остановлены в середине выполнения без какой-либо возможности остановиться.
Привет, параллелизм!
Базовый main «Hello, World!» для параллелизма.
func main() {
ctx, cancel := context.WithCancel(context.Background())
// Do parallel work...
go task(ctx)
cancel()
time.Sleep(5 * time.Second)
}
goplay
Существуют улучшения, которые следует сделать, но это позволит вам породить любое количество горутин и дать пять секунд на их остановку.
Вывод: Всегда создавайте основной отменяемый Контекст, чтобы позволить горутинам изящно останавливаться.
Конечно, горутины не просто останавливаются сами по себе. Они должны отслеживать свой Context на предмет отмены. Хороший способ сделать это — использовать select
. Она будет блокировать выполнение до тех пор, пока одна из ветвей case
не станет доступной.
func task(ctx context.Context) {
for {
select {
case <-ctx.Done():
// Context cancelled. Should now stop processing work.
return
default:
// Process work
}
}
}
goplay
Эта функция будет выполняться до тех пор, пока не будет отменен ее Контекст. Поскольку default
всегда доступен, эта функция сначала проверит канал Done() на наличие значения и, если ничего не готово, выполнит ветвь default
. Даже если задача не выполняется постоянно, всегда полезно проверить Context перед выполнением дорогостоящей операции, такой как запрос к базе данных.
Вывод: Проверяйте отмену контекста до и во время выполнения длительных задач.
Параллельность данных
Когда данные должны быть доступны одной и той же горутине, очень легко обернуть эти данные в sync.Mutex
и на этом закончить. Это может привести к сложному коду и тупикам. Golang предоставляет лучший способ передачи данных между горутинами в виде каналов. Каналы можно рассматривать как передачу данных от одной горутины к другой. Эта передача блокируется с обеих сторон до тех пор, пока обе стороны не будут готовы.
Этот шаблон делает несколько вещей для вас:
- Гарантирует, что только один процесс изменяет данные в одно и то же время
- Обеспечивает логическое разделение работы, которое легко понять
- Развязывает код, который в противном случае полагался бы на определенные мьютексы.
func readPacket(ctx context.Context, packetChannel chan Packet) {
for {
select {
case <-ctx.Done():
// Context cancelled. Should now stop processing work.
return
case packet, ok := <-packetChannel:
if !ok {
return
}
packet.ReadCount++
packetChannel <- packet
}
time.Sleep(time.Millisecond)
}
}
goplay
readPacket()
постоянно проверяет канал на наличие новых пакетов, считывает их, а затем сбрасывает обратно в канал для обработки другой горутиной. Если канал когда-нибудь будет закрыт, то мы должны остановиться. ok
становится ложным, как только это произойдет. Небольшой сон в такой быстрой циклической горутине, как эта, дает время другим горутинам на обработку и предотвращает максимальную нагрузку на процессор.
Вместо того чтобы принимать пакет, блокируя связанный с ним мьютекс, мы можем свободно передавать пакет между горутинами. Гороутина — это изолированный фрагмент работы, выполняемой над пакетом. В других горутинах могут выполняться другие операции, но они никогда не повлияют на безопасность потоков этой горутины.
Вывод: Используйте каналы для логического разделения одновременной работы.
Извлечение: Не общайтесь, разделяя память; вместо этого обменивайтесь памятью, обмениваясь данными.