К чему приводит обилие конкурентности в кодовой базе Go?
Веб-разработчик, фрилансер… Пишу об ИТ и смежных технологиях. В статье поговорим о том, какие проблемы возникают при конкурентном обращении нескольких потоков к одним и тем же данным. В наше время существуют десятки крупных компаний, одновременно обслуживающих миллионы клиентов по всему миру и оказывающих им различные услуги. При этом, многие из них используют Go в качестве основного языка программирования для разработки своих микросервисов, архитектура которых порой работает на нескольких миллионах процессорных ядер. К примеру, монорепозиторий известного перевозчика Uber содержит около 46 миллионов строк кода и около 2100 уникальных сервисов. Использование языка Go для этих целей обусловлено рядом весомых преимуществ: грамотно настроенный параллелизм, простая настройка сборки мусора, минималистичный подход к синтаксису, отличные инструменты для дальнейшей обработки и обслуживания кода, поддержка библиотек, а также растущее с каждым днем, сообщество разработчиков. В своей работе, Go-разработчики для передачи и обмена информацией используют легковесные потоки – горутины. Так вот, при параллельном обращении нескольких горутин к одним и тем же данным и начинают возникать большие проблемы, называемые в Go – состоянием гонки (Race conditions). А учитывая обилие кодовой базы и количество обрабатываемых одновременно процессов – проблема принимает глобальный масштаб. В статье попробуем разобраться, что это за напасть такая и как от нее избавиться. Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека Go разработчика» Интересно, перейти к каналу Прежде чем поговорить о гонках данных (data races), необходимо упомянуть о концепциях параллелизма и конкурентности, с которыми обязательно нужно разобраться, перед тем как заниматься многопоточной разработкой. Эти понятия схожи, но имеют существенные различия – «Concurrency is not Parallelism!». Суть этого расхожего выражения в том, что конкурентность – это методика проектирования вашей программы, параллелизм – это один из способов ее выполнения. Говорить о конкурентности можно, когда работа приложения предполагается с несколькими одновременно запущенными задачами при создании процессов, выполняющихся независимо друг от друга. При этом, для достижения необходимого поведения, процессов в приложении может быть большое количество. В одноядерных системах конкурентное выполнение потоков имеет модель, при которой контекст переключается между задачами, то есть программа может работать сразу с несколькими задачами, однако, не сможет выполнить их все вместе, ведь ядро всего одно. Причем переключается он в этом случае настолько быстро, что может создаться впечатление, что выполняемые процессы завершаются одновременно. При этом говорить о параллелизме мы не можем, ведь задачи не могут выполняться вместе просто потому, что система у нас – одноядерная. Проще говоря, когда мы имеем несколько одновременно выполняющихся потоков инструкций – надо делать так, чтобы код выполнялся параллельно, а этот процесс требует конкурентности. Ведь при отсутствии конкурентного дизайна распараллелить нашу программу не выйдет. А вот сама по себе концепция конкурентности не обязательно нуждается в параллелизме, ведь если программа способна работать на нескольких ядрах, она спокойно может функционировать и на одном. Проблемы, относящиеся к категории состояние гонки, довольно трудно обнаружить и поэтому они относятся к группе самых непредсказуемых ошибок программирования. Довольно часто при запуске кода они вызывают загадочные сбои, понять природу которых не может и матерый разработчик. При этом, даже упрощающий написание чистого кода механизм конкурентности, не сможет предотвратить Race conditions. В таких случаях, от программиста требуется осторожность, больше усердия и грамотное тестирование. Гонка происходит, когда две или более горутины обращаются к одним и тем же данным. Сбои, вызванные гонками данных в программах Go, повторяются и зачастую снижают эффективность и производительность наиболее важных функций, что будет доставлять неудобства вашим клиентам и повлияет на их доход. Порой разработчикам довольно трудно отлаживать data races и иногда некоторые из них исправляют такие ошибки с помощью консервативных методов. Одним из которых является отключение параллелизма в подозрительных областях кода. Для того, чтобы иметь возможность заранее диагностировать приложение для обнаружения такого рода ошибок разработчики языка создали функционал, называемый детектор гонки. Впервые этот набор инструментов, определяющих состояния гонки в Go-коде, появился в версии 1.1 и по сей день, успешно работает на операционках Linux, OS X и Windows с 64-разрядными процессорами x86. В основе детектора лежит библиотека времени выполнения (runtime library) C/C++ – ThreadSanitizer, используемая для обнаружения ошибок в кодовой экосистеме Google и Chromium. Внедрили эту технологию в Go в сентябре 2012, и после этого она стала частью непрерывного процесса сборки по отслеживанию условий гонки при их возникновении. Race detector встроен в цепочку инструментов Go и работает следующим образом: Детектор сконструирован таким образом, что он в состоянии обнаружить Race conditions только при фактическом запуске кода. Поэтому важно осуществлять запуск двоичных файлов приложения при реалистичных рабочих нагрузках. Но двоичный файл с детектором гонки может сильно влияет на производительность процессора и памяти, вызывая перегрузку системы – поэтому не стоит держать детектор гонки включенным постоянно. Хорошей практикой является запуск его, вместе с нагрузочными и интеграционными тестами, ведь они, в большинстве своем, используют конкурентные части кода. Как мы уже упоминали ранее, для того, чтобы подключить к необходимому участку кода включенный детектор гонки, к нему нужно добавить флаг Рассмотрим пример упрощенной версии фактической ошибки, которую обнаружил детектор гонки. Здесь он с помощью таймера выводит сообщения с произвольным интервалом от 0 до 1 секунды. Все это действие повторяется пять секунд. Он использует *** Race detector – мощнейший инструмент, проверяющий корректность всех параллельных программ, к предупреждениям которого нужно отнестись со всей серьезностью, ведь ложных срабатываний он не выдает. Обратите внимание, что детектор находит лишь те гонки, которые могут появиться в процессе выполнения приложения, поэтому он не может найти их на участках кода, которые не выполняются. Denver 83
Проблемы многопоточной разработки
Параллелизм и конкурентность
Состояния гонки в Go
Детектор гонки
-race
, показывающий компилятору полный список обращений к памяти с помощью кода, описывающего основные параметры осуществления доступ к памяти.ThreadSanitizer
ищет в коде несинхронизированные обращения к общим переменным и, обнаружив такое «грубое» поведение, выдает предупреждение.Как использовать детектор гонки
-race
, например:
$ go test -race pack // тестируем пакет $ go run -race src.go // компилируем и запускаем программу $ go build -race cmd // сборка команды $ go install -race pack // устанавливаем пакет
time.AfterFunc
для создания Timer
для первого сообщения, а затем использует метод Reset
для планирования следующего сообщения, каждый раз повторно используя Timer
.
func main() { start := time.Now() var t *time.Timer t = time.AfterFunc(randomDuration(), func() { fmt.Println(time.Now().Sub(start)) t.Reset(randomDuration()) }) time.Sleep(5 * time.Second) } func randomDuration() time.Duration { return time.Duration(rand.Int63n(1e9)) }
Материалы по теме
- 1 views
- 0 Comment