Перейти к содержанию

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

Программа работает часами, а потом раз в день выдаёт странное значение. Частая причина — состояние гонки: основной код и обработчик прерывания обращаются к одним данным, и результат зависит от того, в какой момент прерывание вклинилось.

Обновлено Редакция GAW

Состояние гонки (race condition) — ошибка, при которой результат программы зависит от того, в какой момент одно действие вклинилось в другое. В прошивках микроконтроллеров её почти всегда создают прерывания: основной код и обработчик пользуются одной переменной, а операция над ней не атомарна. Такие ошибки проявляются редко, зависят от нагрузки и времени и плохо ловятся отладчиком.

Три типичных случая

  1. Разорванное чтение. 8-битный AVR читает 16-битную переменную двумя командами. Если обработчик меняет её между ними, получается смесь двух значений: в нашем примере 0x00FF → 0x0100 даёт прочитанное 0x01FF = 511. На Cortex-M то же бывает с 64-битными переменными и структурами.
  2. Потерянное изменение. Основной код выполняет count++ — чтение, сложение, запись. Если между чтением и записью обработчик тоже увеличил count, его изменение затрётся.
  3. Сон без пробуждения. Программа проверила флаг, решила уснуть, а событие пришло между проверкой и командой сна — микроконтроллер спит с необработанным событием.

Как избежать

ПриёмЧто защищает
Критическая секция (ATOMIC_BLOCK, PRIMASK)чтение и изменение многобайтных данных
Одиночный доступ к байту или выровненному словупростые флаги
LDREX/STREX на Cortex-M3/M4счётчики без запрета прерываний
cli — проверка — sei — sleepуход в сон

Последний приём работает потому, что по даташиту ATmega328P команда сразу после SEI выполняется до любого прерывания: команда сна идёт вместе с разрешением прерываний, и событие уже не потеряется.

Не путать состояние гонки с проблемой volatile: без volatile компилятор может вообще не увидеть изменение переменной из обработчика, а гонка возникает и с volatile — из-за неделимости операций. Разбор всех случаев с кодом для AVR и STM32, кольцевой буфер и проверки — в статье «Прерывания и общие данные».

Частые вопросы

Как выглядит состояние гонки на практике?

Пример (наш расчёт): основная программа на AVR читает 16-битный счётчик 0x00FF двумя командами. Прочитав младший байт 0xFF, она прерывается, обработчик увеличивает счётчик до 0x0100, и программа дочитывает старший байт 0x01. Получается 0x01FF = 511 вместо 255 или 256. Ошибка редкая и плохо воспроизводится.

Поможет ли volatile от гонки?

Нет: volatile не делает операцию неделимой — подробно в термине о volatile. От гонки защищают критическая секция или атомарные команды.

Что такое гонка при уходе в сон?

Программа проверяет флаг «есть работа» и, если его нет, засыпает. Если прерывание установит флаг между проверкой и командой сна, программа уснёт с необработанным событием. avr-libc предлагает порядок cli — проверка — sleep_enable — sei — sleep_cpu: команда сразу после sei выполняется до любого прерывания, поэтому сон и разрешение прерываний происходят вместе.

Источники

  1. AVR-LibC 2.3.2: <avr/interrupt.h>, <util/atomic.h>, <avr/sleep.h>, Interrupts (interrupts.dox), Problems with Reordering Code (reordering-code.dox) — util/atomic.h: пример — 16-битный счётчик, изменяемый в обработчике, читают в ATOMIC_BLOCK; avr/sleep.h: проверка условия и уход в сон без гонки — cli; проверка; sleep_enable; sei; sleep_cpu (после sei выполняется ещё одна команда)
  2. Microchip. ATmega48A/PA/88A/PA/168A/PA/328/P Data Sheet, DS40002061B, 2020 — после SEI выполняется следующая команда до любого прерывания; доступ к 16-битным регистрам через TEMP требует запрета прерываний в основной программе
  3. Arm. ARMv7-M Architecture Reference Manual, DDI 0403E.e, 2021 — A3.5.3: атомарны только одиночные обращения к байту, выровненному полуслову и слову

Нашли ошибку?