Состояние гонки (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. Предположим, что выполнение программы происходит в таком порядке:

    1. Оператор if в потоке 2 проверяет x на чётность.
    2. Оператор «x++» в потоке 1 увеличивает x на единицу.
    3. Оператор вывода в потоке 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 МэВ, что вело к переоблучению. При этом датчики выводили «Нулевая доза», поэтому оператор мог повторить процедуру, усугубляя ситуацию. В результате погибли как минимум четыре пациента (подробнее в викепедии)

wikipedia: Состояние гонки