Проблема параллелизма - тихий убийца вашей программы
Наша команда создает доставку еды по запросу в Таиланде, и мы постоянно сталкиваемся с проблемами параллелизма. Самый младший разработчик, с которым я разговаривал, похоже, не знал об этой проблеме, поэтому мы их обучили. Однажды наша команда решила разработать новое упражнение на собеседовании, счетчик просмотров страниц, которое включает некоторые расширения проблемы параллелизма. Мы обнаружили, что очень мало инженеров могут правильно написать систему. Я пришел сюда, чтобы написать об этом, чтобы повысить осведомленность в этой области.
Для простоты я продемонстрирую проблему на примере Go. Итак, вот требования к упражнению со счетчиком просмотров страниц:
- Вы создаете очень простой HTTP-сервер и можете использовать любую веб-платформу.
- Конечных точек всего 2: домашняя страница и страница статистики.
- Вы можете сохранить статистику в памяти, чтобы ее можно было сбросить до нуля после перезапуска сервера.
- Многие пользователи посещают вашу домашнюю страницу.
Я буду использовать Джин как основу, а вот код, который удовлетворяет трем основным требованиям.
Запустите сервер, используя команду ниже, и перейдите по адресу http: // localhost: 8080 / и http: // localhost: 8080 / stats
go run main.go
В этом коде вроде нет ничего плохого, но только вы сами используете его при тестировании. Кстати, производственная система, здесь одновременно много людей используют.
Давайте смоделируем, отправив некоторую нагрузку на сервер. Установите Apache Bench на свой компьютер и отправьте нагрузку с помощью этой команды.
ab -n 1000 -c 20 http://localhost:8080/
Приведенная выше команда отправит в общей сложности 1000 запросов на домашнюю страницу от 20 одновременных пользователей (что означает, что 20 пользователей открывают вашу домашнюю страницу одновременно).
Запустите его и проверьте статистику. Вы получаете 1000 просмотров страниц? Какой у тебя номер? Менее 1000? Да, должно быть меньше 1000, но точное число неизвестно.
Под капотом
Оказывается, counter++ не является «атомарным» и не «синхронизированным», поэтому несколько ядер ЦП могут одновременно читать и записывать одно и то же значение в основную память и из нее.
Обычно counter++ выполняет 3 основных шага:
- Загружает значение
counterиз основной памяти в регистр ЦП - Увеличивает значение на 1 на ЦП
- Копировать значение из ЦП обратно в
counterв основной памяти
Если есть 2 потока, обрабатывающие запросы и работающие на разных процессорах. Вы можете увидеть график каждой выполняемой инструкции ЦП, как показано ниже:
|
|
LOAD "counter" |
#AX=0 |
| LOAD "counter"
| #AX=0
|
INC AX |
#AX=1 |
| INC AX
| #AX=1
|
MOV AX to "counter" |
#counter=1 |
| MOV AX to "counter"
| #counter=1
|
|
\ | /
\|/
-
CPU 1 CPU 2
В конце оба процессора просто обновляют значение counter до 1 вместо 2, что приводит к тому, что счетчик статистики становится меньше, чем реальный счетчик попаданий. Чем больше совпадений, тем больше вероятность неправильного подсчета.
Ну как мы можем это исправить?
Решение
Большинство языков программирования поставляются с примитивом синхронизации и атомарным типом данных. В Go есть пакеты sync и sync/atomic. В этом случае sync/atomic дает лучшую производительность, но я покажу вам оба решения.
синхронизация / атомарный
atomic предоставляет функции для атомарного чтения / записи примитивной переменной, что предотвращает проблему, когда несколько процессов записывают одно и то же значение в основную память.
Вам нужно изменить тип counter переменной на int32, который поддерживается sync/atomic, а затем использовать AddInt32(*int32) и LoadInt32(*int32) для изменения и чтения переменной соответственно.
Использование Mutex из пакета синхронизации
Вы объявляете разблокированный мьютекс и вызываете mutex.Lock() перед изменением переменной. Убедитесь, что вы не забыли снять блокировку, используя defer вместе с mutex.Unlock(), чтобы после обработки первого запроса можно было обработать следующий запрос.
Счетчик просмотров страниц - простой пример, который дает вам некоторое представление об этой проблеме. Все переменные и типы данных, используемые несколькими потоками, могут столкнуться с проблемой, например, когда вы обновляете данные на карте, изменяете срез / массив, обновляете данные в БД. Чтобы гарантировать отсутствие проблем с параллелизмом, можно использовать специальный тип данных, который является «потокобезопасным».
Проблема с параллелизмом довольно сложно отлаживать на вашем локальном компьютере, потому что для этого требуется отправка некоторой нагрузки на сервер с некоторой вероятностью, но если вы подумаете о проблеме на раннем этапе разработки, это поможет вам предотвратить проблему, чтобы вы могли сосредоточиться вместо этого на оказании влияния на бизнес.
Надеюсь, у вас появятся идеи. Продолжайте кодировать!