Проснуться сегодня в 2 часа ночи оказалось на удивление продуктивным. Так здорово работать над проектами на работе, которые каждый день учат чему-то новому. Здесь, в этом блоге, я поделюсь интересной информацией о том, что я недавно позаботился в своем проекте — достижение параллельной логики потокобезопасности!

Давай начнем!

Постановка задачи

Создайте 10 000 горутин, в которых каждая горутина генерирует срез из трех случайных целых чисел, и поставьте их все в одну очередь. Таким образом, в конце выполнения всех 10 000 горутин в очереди будет 30 000 записей.

Чтобы представить это в перспективе, это классический случай проблемы параллельной очереди.

Первый подход

Первый подход всегда наивен!

  1. Создайте структуру данных и функцию для постановки в очередь.
  2. Затем в функции main определите цикл for, который выполняется 10 тыс. раз, запуская goroutine на каждой итерации для создания среза из трех целых чисел и постановки их в очередь на срез (в данном случае в очередь)
  3. После завершения выполнения всех горутин выведите длину очереди, чтобы убедиться, что она равна 30 КБ.

Весь файл go будет выглядеть следующим образом:

Когда мы запускаем это (попробуйте здесь), мы видим, что на выходе отображается что-то вроде 23346 или любое другое число, которое меньше 30k, что не ожидается!

Итак, что здесь не так?

Проблема здесь в том, что есть несколько горутин, которые пытаются записать свои фрагменты в очередь в одно и то же время по одному и тому же индексу. Таким образом, возникает состояние гонки, когда данные победителя ставятся в очередь, а данные проигравшего теряются.

По сути, эта реализация не является потокобезопасной!

Теперь давайте рассмотрим два разных подхода к тому, чтобы сделать это решение потокобезопасным. Подходы

  1. Использование мьютекса
  2. Использование каналов Go

Подход № 1 — использование мьютекса

Возьмите приведенное выше решение и

  1. Добавить блокировку типа sync.Mutex в структуру данных Queue
  2. Вызовите методы Lock и Unlock в функции Enqueue, чтобы убедиться, что никакие две горутины не ставятся в очередь одновременно. Если одна горутина ставит в очередь очередь, то другие горутины будут ждать, пока первая не освободит блокировки. Таким образом, никаких накладок не будет.

Новое решение будет выглядеть

Теперь, если мы запустим этот код (попробуйте здесь), мы увидим ровно 30k, как и ожидалось, как длина очереди. Это здорово! 🕺

Мы добились здесь потокобезопасности, хотя одновременно запускаем 10 тысяч горутин! 😻 Это было все о создании потокобезопасных параллельных горутин с использованием Mutex (или блокировок с точки зрения непрофессионала). Теперь давайте посмотрим, как добиться того же с помощью каналов.

Подход № 2 — Использование каналов Go

В этом подходе мы отказываемся от использования мьютексов и блокировок, с которыми нужно обращаться более осторожно, и используем каналы Go, которые являются более значимым и бесшовным способом обработки связи между одновременными выполнениями Go.

В этом подходе мы

  1. Определите Go Buffer Channel длиной 10k и типом Slice of integers []int
  2. В каждой горутине мы назначаем новый фрагмент из 3 целых чисел этому каналу и ждем завершения всех горутин, используя метод ожидания группы ожидания.
  3. Наконец, мы перебираем буферизованный канал и ставим в очередь Slice из 3 целых чисел, сгенерированных из каждой горутины в основной очереди.

Теперь код выглядит так

И когда мы запускаем это (попробуйте здесь), мы снова видим, что длина очереди составляет 30 КБ, как и ожидалось, потому что буферизованный канал позаботился о параллельном выполнении горутин. Хотя нам пришлось повторить итерацию дважды, с точки зрения сложности это все равно остается таким же, как и один раз.

Итак, еще раз, Ура! Мы смогли добиться безопасного одновременного выполнения горутин.

Заключение

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

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

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

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

Если вам понравилось это читать, ставьте аплодисменты!

С уважением и благодарностью,
Самарт Деягонд

Еще от автора