Гонки, критические секции и разделяемая память
Вступление
Два процесса или два потока видят одну и ту же переменную в памяти. Каждый делает счётчик++. Логически должно получиться +2. На практике часто +1 — и это гонка данных (race condition).
Операционная система даёт разделяемую память (shared memory) и быстрый IPC. Ответственность за корректность — на программисте: нужны критические секции и примитивы синхронизации, которые ядро реализует атомарно.
Эта статья — про концепции уровня ОС. Примеры на Java: 5-03-java/23; асинхронность в приложениях: 4-05-asinhronnost.
Связано: управление процессами, тупики, ядро и IPC.
Почему «просто увеличить счётчик» не работает
На уровне процессора counter++ — это несколько шагов:
- загрузить значение из RAM в регистр;
- увеличить регистр;
- записать обратно в RAM.
Два потока могут перемешаться:
| Время | Поток A | Поток B | counter в памяти |
|---|---|---|---|
| 1 | read → 10 | 10 | |
| 2 | read → 10 | 10 | |
| 3 | write 11 | 11 | |
| 4 | write 11 | 11 (ожидали 12) |
Потерянное обновление (lost update) — классический симптом гонки.
Критическая секция
Критическая секция — фрагмент кода, где программа обращается к общему ресурсу (переменная, структура, устройство). Требования к решению (Dijkstra):
- Взаимное исключение — в критической секции одновременно не больше одного исполнителя.
- Прогресс — если никто не в секции, выбор следующего не должен зависнуть навсегда.
- Ограниченное ожидание — нет бесконечного starvation из-за блокировки.
На практике это реализуют блокировками, которые поддерживает ОС.
Уровни решения
| Уровень | Пример | Когда уместно |
|---|---|---|
| Отключение прерываний | Только в ядре, на одном CPU | Микросекунды, нельзя в user space |
| Атомарные инструкции | LOCK CMPXCHG, C11 atomic | Счётчики, lock-free структуры |
| Примитивы ОС | mutex, semaphore, futex | Общий случай в приложениях |
| Высокоуровневые API | synchronized, std::mutex | Поверх примитивов ОС |
Пользовательская программа не должна отключать прерывания — это привилегия ядра.
Разделяемая память в POSIX
Самый быстрый IPC — **отобразить одну область в два адресных пространства:
// Упрощённая схема: родитель создаёт сегмент, fork, оба видят int *counter
int *counter = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
*counter = 0;
if (fork() == 0) {
for (int i = 0; i < 100000; i++)
(*counter)++; // гонка без синхронизации!
}
MAP_SHARED — изменения видны другому процессу. Без mutex или атомиков результат недетерминирован.
Тот же счётчик с pthread_mutex (детерминированно +2 за два потока по 100000 итераций):
#include <pthread.h>
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int *counter; /* отображён через mmap MAP_SHARED, как выше */
void *worker(void *arg) {
(void)arg;
for (int i = 0; i < 100000; i++) {
pthread_mutex_lock(&lock);
(*counter)++;
pthread_mutex_unlock(&lock);
}
return NULL;
}
Альтернативы: shm_open + mmap, System V shmget/shmat — см. 5115.
Процедурные примитивы синхронизации
Мьютекс (mutual exclusion)
Двоичная блокировка: свободен / занят.
lock()— если занят, поток блокируется (ядро снимает с CPU);unlock()— будит одного ожидающего.
В Linux pthread: pthread_mutex_t; в ядре — futex (fast userspace mutex): попытка в user space, syscall только при contention.
Не путать с взаимной блокировкой (deadlock) — это ситуация, когда процессы ждут друг друга по кругу. Мьютекс — инструмент; deadlock — ошибка использования. См. 5119.
Семафор
Счётчик ресурсов: wait (P) уменьшает, при 0 блокирует; signal (V) увеличивает, будит ожидающих.
- Бинарный семафор ≈ мьютекс (с нюансами владения).
- Счётный — пул из N одинаковых слотов (N соединений к БД, N буферов).
Барьер
Все N участников должны дойти до точки — только потом любой продолжает. Удобно для фаз параллельных алгоритмов (pthread_barrier).
Условная переменная (condition variable)
Ждать условия под мьютексом: «очередь не пуста», «есть свободный слот». wait атомарно отпускает mutex и засыпает; signal/broadcast будит.
Паттерн producer-consumer:
producer: lock(mutex); положить в буфер; signal(cond); unlock(mutex);
consumer: lock(mutex); while (пусто) wait(cond, mutex); взять; unlock(mutex);
Монитор (учебное понятие)
Монитор — языковая конструкция: модуль + mutex + условные переменные внутри; только один поток внутри монитора. В Java — synchronized на метод/блок. В чистом C — комбинация mutex + cond вручную.
Очереди сообщений
Данные копируются в ядро — медленнее shared memory, но не нужна общая память и проще избежать гонок на структурах (если одно сообщение — атомарная единица). См. msgsnd/msgrcv в 5115.
Сравнение примитивов
| Примитив | Типичное использование | Риск deadlock |
|---|---|---|
| Mutex | Защита структуры данных | При нескольких mutex без порядка |
| Semaphore | Пул ресурсов, сигнализация | Тот же |
| Cond var | Ожидание условия | Вместе с mutex — да, если нарушен порядок |
| Barrier | Синхронизация фаз | Редко |
| Message queue | Обмен без shared state | Блокировки в другом месте |
Типичные ошибки
- Забыли синхронизировать shared memory — недетерминизм в тестах.
- Держать lock слишком долго — падает параллелизм, растёт latency.
- Два mutex в разном порядке в разных потоках — deadlock.
- Spinlock в user space на одном ядре — busy-wait сжигает CPU (в ядре на коротких критических секциях — норма).
- Сигналы и mutex — не все функции async-signal-safe; в обработчике сигнала нельзя
pthread_mutex_lockбез осторожности.
Как ОС помогает
- Планировщик снимает поток с CPU при блокировке на mutex — другие работают.
- Приоритетное наследование — снижает инверсию приоритетов (частично).
- Атомарные операции в CPU и барьеры памяти — основа lock-free.
- Инструменты:
strace, Thread Sanitizer (TSan),helgrind— ловят гонки на тестах.
Практический вывод
| Вопрос | Ответ |
|---|---|
| Нужна ли синхронизация? | Да, если несколько исполнителей пишут в общее состояние |
| Что выбрать первым? | Mutex + чёткая критическая секция |
| Когда shared memory? | Большие объёмы, низкая latency |
| Когда очередь сообщений? | Изоляция, границы сервисов |
Дальше: тупики и защита, управление процессами.
См. также
Другие статьи этого же раздела в боковом меню (как на странице «О разделе»). Программное обеспечение, управляющее аппаратными ресурсами компьютера. Основные функции и задачи ОС. Функциональные и нефункциональные требования к операционным системам, критерии выбора архитектуры ядра и способы реализации подсистем. Классификация операционных систем - ключевые семейства ОС, их отличия, типовые области применения и архитектурные особенности. Основы UNIX-систем - ключевые принципы многозадачности, иерархии файлов и управления процессами в классической Unix-модели. Ядро операционной системы - различия монолитной и микроядерной архитектуры, их компромиссы по производительности и надежности. Обзор Windows — версии, компоненты ядра NT, файловая система NTFS, структура каталогов и отличия от Unix-подобных систем. Полный инструментарий по Windows 11, возможности и функции. Устройство файловой системы Windows - иерархия хранения данных, служебные структуры и поведение файловой среды в ОС. Работа памяти в Windows - физические и виртуальные уровни, страницы памяти и механизмы управления ресурсами процессов. Локализация и символы в Windows - особенности кодировок, терминалов и корректной обработки текста в системных инструментах. Сравнение Windows и Linux - различия подходов к интерфейсу, администрированию и повседневным рабочим сценариям. Linux - структура файловой системы, ключевые каталоги и базовые принципы организации среды в Unix-подобной ОС.Операционные системы
Требования к ОС и подходы к реализации
Классификация операционных систем
Основы UNIX-систем
Ядро операционной системы
Windows
Справочник по Windows 11
Устройство файловой системы Windows
Работа памяти в Windows
Поддержка локализации и символов в Windows
Сравнение Windows и Linux
Linux