Состояние гонки (Race conditional)
Состоя́ние го́нки (англ. race condition) — ошибка проектирования многопоточной системы или приложения, при которой работа системы или приложения зависит от того, в каком порядке выполняются части кода. Своё название ошибка получила от похожей ошибки проектирования электронных схем (см. Гонки сигналов).
Состояние гонки — «плавающая» ошибка (гейзенбаг), проявляющаяся в случайные моменты времени и «пропадающая» при попытке её локализовать.
Пример
Рассмотрим пример кода (на Java).
volatile int x;
// Поток 1:while (!stop){ x++; …}
// Поток 2:while (!stop){ if (x%2 == 0) System.out.println("x=" + x); …}
Пусть x=0. Предположим, что выполнение программы происходит в таком порядке:
- Оператор if в потоке 2 проверяет x на чётность.
- Оператор «x++» в потоке 1 увеличивает x на единицу.
- Оператор вывода в потоке 2 выводит «x=1», хотя, казалось бы, переменная проверена на чётность.
Способы решения
Локальная копия
Самый простой способ решения — копирование переменной x в локальную переменную. Вот исправленный код:
// Поток 2:while (!stop){ int cached_x = x; if (cached_x%2 == 0) System.out.println("x=" + cached_x); …}
Естественно, этот способ работает только тогда, когда переменная одна и копирование производится за одну машинную команду.
Синхронизация
Более сложный, но и более универсальный метод решения — синхронизация потоков, а именно:
int x;
// Поток 1:while (!stop){ synchronized(SomeObject) { x++; } …}
// Поток 2:while (!stop){ synchronized(SomeObject) { if (x%2 == 0) System.out.println("x=" + x); } …}
Здесь семантика happens before не требует ключевое слово volatile
Комбинированный способ
Предположим, что переменных — две (и ключевое слово volatile
не действует), а во втором потоке вместоSystem.out.println
стоит более сложная обработка. В этом случае оба метода неудовлетворительны: первый — потому что одна переменная может измениться, пока копируется другая; второй — потому что засинхронизирован слишком большой объём кода.
Эти способы можно скомбинировать, копируя «опасные» переменные в синхронизированном блоке. С одной стороны, это снимет ограничение на одну машинную команду, с другой — позволит избавиться от слишком больших синхроблоков.
volatile int x1, x2;
// Поток 1:while (!stop){ synchronized(SomeObject) { x1++; x2++; } …}
// Поток 2:while (!stop){ int cached_x1, cached_x2; synchronized (SomeObject) { cached_x1 = x1; cached_x2 = x2; } if ((cached_x1 + cached_x2)%100 == 0) DoSomethingComplicated(cached_x1, cached_x2); …}
Очевидных способов выявления и исправления состояний гонки не существует. Лучший способ избавиться от гонок — правильное проектирование многозадачной системы.
Случай с Therac-25
Аппарат лучевой терапии Therac-25 был первым в США медицинским аппаратом, в котором вопросы безопасности были возложены исключительно на программное обеспечение.
Из-за состояния гонки между управляющей программой и обработчиком клавиатуры иногда случалось, что пациент напрямую облучался пучком электронов в 25 МэВ, что вело к переоблучению. При этом датчики выводили «Нулевая доза», поэтому оператор мог повторить процедуру, усугубляя ситуацию. В результате погибли как минимум четыре пациента (подробнее в викепедии)