Избегайте состояний гонки
Причина
Если вы этого не сделаете, ничто не гарантировано работать и утонченные ошибки будут сохраняться.
Примечание
В двух словах, если два потока могут одновременно (без синхронизации) получить доступ к одному и тому же объекту, и по крайней мере один является писателем (выполняет неконстантную операцию), у вас есть состояние гонки. Для дальнейшей информации о том, как хорошо использовать синхронизацию для устранения состояний гонки, пожалуйста, обратитесь к хорошей книге о параллелизме (см. Тщательно изучайте литературу).
Плохой пример
Есть много примеров состояний гонки, которые существуют, некоторые из которых работают в производственном ПО в этот самый момент. Один очень простой пример:
int get_id()
{
static int id = 1;
return id++;
}
Увеличение здесь - это пример состояния гонки. Это может пойти не так множеством способов, включая:
- Поток A загружает значение
id, ОС переключает контекст A на некоторый период, за который другие потоки создают сотни ID. Затем A разрешено снова запуститься, иidзаписывается обратно в это место как прочитанное Aidплюс один. - Поток A и B загружают
idи увеличивают его одновременно. Они оба получают один и тот же ID.
Локальные статические переменные - это общий источник состояний гонки.
Плохой пример:
void f(fstream& fs, regex pattern)
{
array<double, max> buf;
int sz = read_vec(fs, buf, max); // read from fs into buf
gsl::span<double> s {buf};
// ...
auto h1 = async([&] { sort(std::execution::par, s); }); // spawn a task to sort
// ...
auto h2 = async([&] { return find_all(buf, sz, pattern); }); // spawn a task to find matches
// ...
}
Здесь у нас есть (неприятное) состояние гонки на элементах buf (sort будет и читать и писать). Все состояния гонки неприятны. Здесь мы управляли получить состояние гонки на данных на стеке. Не все состояния гонки так легко заметить, как это.
Плохой пример:
// code not controlled by a lock
unsigned val;
if (val < 5) {
// ... other thread can change val here ...
switch (val) {
case 0: // ...
case 1: // ...
case 2: // ...
case 3: // ...
case 4: // ...
}
}
Теперь, компилятор, который не знает, что val может измениться, скорее всего реализует этот switch с помощью таблицы переходов с пятью записями. Тогда, val вне диапазона [0..4] вызовет переход на адрес, который может быть где угодно в программе, и выполнение продолжится там. Действительно, "все ставки отключены", если вы получите состояние гонки. На самом деле, это может быть ещё хуже: посмотрев на сгенерированный код вы можете определить, где побежит блуждающий переход для заданного значения; это может быть проблемой безопасности.
Применение
Коечто возможно, сделайте хотя бы что-то. Есть коммерческие и открытые инструменты, которые пытаются решить эту проблему, но имейте в виду, что решения имеют стоимость и слепые пятна. Статические инструменты часто имеют много ложных срабатываний, а инструменты времени выполнения часто имеют значительную стоимость. Мы надеемся на лучшие инструменты. Использование нескольких инструментов может поймать больше проблем, чем одного.
Есть другие способы, которыми вы можете снизить шанс состояний гонки:
- Избегайте глобальных данных
- Избегайте
staticпеременных - Больше использование конкретных типов на стеке (и не передавайте указатели вокруг слишком много)
- Больше использование неизменяемых данных (литералы,
constexpr, иconst)