Операционные системы
Теория: Как ОС делает всё одновременно
Многозадачность в системе — это работа планировщика (scheduler). Он решает, какой поток получит процессор прямо сейчас. Процессор не выполняет всё одновременно — он быстро переключается между потоками, выдавая каждому небольшой отрезок времени — квант (обычно 1–10 миллисекунд). Когда один поток блокируется (например, ждёт чтение с диска), CPU мгновенно переключается на другой.
Если у вас многоядерный процессор, потоки действительно могут работать параллельно: каждое ядро исполняет свой поток. Поэтому если в htop вы видите, что один процесс использует 400% CPU — значит, у него четыре активных потока, и каждый занят своим ядром.
Linux и macOS планируют потоки через структуру task_struct, Windows — через объект ETHREAD, но смысл одинаков: именно потоки конкурируют за процессор, не процессы
Гонка данных и синхронизация потоков
Когда несколько потоков выполняются внутри одного процесса, они делят общую память: глобальные переменные, динамическую кучу, открытые файлы и сетевые соединения. Это делает многопоточность быстрой и эффективной — ведь не нужно копировать данные между процессами. Но эта же особенность превращается в источник ошибок, если потоки начинают работать с одними и теми же данными одновременно.
Представьте: два потока увеличивают значение одного счётчика. Каждый из них выполняет миллион операций. Наивно ожидается, что итоговое значение будет два миллиона. Однако результат почти всегда меньше — иногда 1,4 млн, иногда 1,7 млн, а иногда даже 900 тысяч. Это классическая гонка данных (race condition): результат зависит от того, какой поток успел прочитать и записать значение первым.
Всё дело в том, что операция counter += 1 не является атомарной. Процессор выполняет её в несколько шагов: читает значение из памяти, прибавляет единицу и записывает результат обратно. Если между этими шагами влезает другой поток, один из инкрементов просто теряется.
Посмотрим, как это выглядит на практике.
Пусть два потока увеличивают один и тот же счётчик:
- первый делает 1 000 000 инкрементов
- второй делает 1 000 000 инкрементов
- ожидаем итог: 2 000 000
Но почти никогда такого не получим. Почему?
Потому что выражение:
не атомарное. Оно распадается на три шага:
- взять значение
counterиз памяти - прибавить 1
- записать новое значение обратно
Если в этот момент вмешался другой поток, запись теряется.
Давайте проверим на практике
Что увидите на выводе:
- очень редко: 2 000 000 (почти невозможно)
- обычно: меньше — 1 300 000, 1 700 000, 900 000...
То есть часть операций потерялась. Один поток перезаписал результат другого. Это и есть race condition.
Мьютекс: как решить проблему гонки
Чтобы потоки не ломали общие данные, операционная система предоставляет механизмы синхронизации. Самый простой и распространённый — мьютекс (mutex, mutual exclusion).
Идея простая:
Пока один поток работает в критической секции, остальные ждут. То есть доступ к опасному месту становится эксклюзивным.
Что изменилось
- оба потока всё ещё работают параллельно
- но в момент изменения переменной доступ ограничен
- второй поток ждёт, пока первый выйдет из блока
with lock - итоговые инкременты не теряются
- результат всегда ровно **2 000 000 **
Такой же принцип работает в любом языке и любой ОС — C, C++, Java, Go, Rust, Windows, Linux. Везде идея одна: мьютекс не даёт двум потокам одновременно трогать общие данные.
Почему синхронизация не бесплатна
Каждый вызов lock.acquire() и lock.release() требует обращения к ядру и синхронизации с другими потоками. Это занимает микросекунды, но при тысячах обращений в секунду эффект становится заметным. Чем больше потоков и чем чаще они блокируются, тем ниже общая производительность. Именно поэтому многопоточные серверы вроде nginx, MySQL или Redis стараются минимизировать количество общих переменных и критических секций, чтобы потоки могли работать параллельно, не дожидаясь друг друга.
В реальной инфраструктуре подобные проблемы проявляются не в коде, а в поведении сервисов. Если под нагрузкой приложение «виснет» без ошибок, перестаёт отвечать или ведёт себя непредсказуемо, дело может быть не в сети и не в БД, а в гонке данных или взаимной блокировке потоков внутри кода.
Взаимная блокировка и потокобезопасность
Синхронизация решает одну проблему — гонку данных, но при неправильном применении создаёт другую, куда более неприятную — взаимную блокировку, или deadlock. Это ситуация, когда несколько потоков ждут друг друга и никто не может продолжить работу. С точки зрения ядра всё выглядит идеально: процесс жив, ошибок нет, CPU свободен. Но приложение застыло навсегда.
Представьте, что у нас есть два потока. Первый должен записать данные в файл A, потом обновить индекс в файле B. Второй делает обратное — сначала обновляет индекс, потом пишет данные.
Каждый поток блокирует файл, чтобы не повредить данные, но порядок блокировок разный. И всё — мы получили классическую взаимную блокировку.
Пример на Python
Что произойдёт
- Поток A успевает захватить file_a_lock.
- Поток B в это время захватывает file_b_lock.
- Через секунду оба пытаются взять второй замок — и каждый ждёт другого.
Результат: программа зависает навсегда. На экране вы увидите:
A: пытается захватить файл A...
A: файл A захвачен
B: пытается захватить файл B...
B: файл B захвачен
A: пытается захватить файл B...
B: пытается захватить файл A...
И дальше — тишина. CPU не нагружен, ошибок нет. Если это происходит в сервере или CI-агенте — он просто “умирает стоя”.
Как выглядит в реальной системе
То же самое бывает в продакшене. Например:
- База данных: два SQL-транзакции обновляют таблицы в разном порядке (UPDATE orders → UPDATE customers и наоборот). В логах — «всё чисто», но транзакции висят.
- Файловые операции: один поток держит lock на файл логов, другой — на конфиг, и оба ждут друг друга.
- Сетевые драйверы или пулы соединений: один поток занял сокет и ждёт ресурс пула, другой наоборот.
Потокобезопасный код (thread-safe)
Функция или модуль считаются thread-safe, если могут работать корректно при одновременном доступе из разных потоков. Если в коде используются глобальные переменные, общий кэш или общие структуры данных без блокировок — это небезопасно.
Потокобезопасный код:
- защищает критические участки мьютексами или атомарными операциями;
- минимизирует общую память между потоками;
- избегает состояния гонки и взаимных блокировок.
Для DevOps это важно не только теоретически. Если сервис “глючит” под нагрузкой, но не падает — вероятно, он столкнулся с race condition или deadlock внутри. Такие ошибки внешне выглядят как проблемы сети, балансировщика или базы, но на деле живут внутри кода приложения.
Мониторинг потоков и процессов
Понять, что происходит в системе, можно без профилировщиков — обычными утилитами.
Linux
macOS
Windows (PowerShell)
Если процесс загружает CPU не полностью, но всё равно “зависает”, стоит проверить, сколько у него потоков и не блокируют ли они друг друга.
Демоны, фоновые процессы и сигналы
Большинство процессов, которые работают постоянно, — это демоны. Они запускаются при старте системы и работают в фоне: sshd, cron, systemd-journald, dockerd — все они живут, пока работает ОС.
Посмотреть активные демоны:
Процессы управляются сигналами:
В Windows для тех же целей служат команды:
Зомби и лимиты
Когда дочерний процесс завершается, но родитель не вызвал wait(), он остаётся в состоянии Z (zombie) — уже не живой, но запись о нём остаётся в таблице процессов. Много зомби — признак ошибок в управлении процессами.
Проверить:
Кроме того, каждый поток занимает память под стек:
- в Linux — около 8 МБ по умолчанию,
- в Windows — около 1 МБ.
Если программа создаёт тысячи потоков, память быстро заканчивается.
Проверить лимиты:

