Как ОС планирует потоки и переключает контекст
Откуда берётся «одновременность» на одном ядре: планировщик и переключение контекста.
Контекстное переключение — это сохранение состояния текущего потока (регистров, счётчика команд) и загрузка состояния другого, чтобы процессор продолжил выполнять уже его.
Потоков обычно гораздо больше, чем ядер. Чтобы все они продвигались, операционная система применяет планировщик: он выдаёт каждому потоку небольшой отрезок процессорного времени — квант (timeslice), а потом передаёт ядро следующему.
Вытесняющая многозадачность
Современные ОС используют вытесняющее (preemptive) планирование: поток не обязан добровольно отдавать управление. Когда квант истекает, таймерное прерывание «вытесняет» поток, даже если он в середине работы. Это защищает систему от потока, который не хочет уступать.
Ядро (одно), кванты по очереди:
| T1 | T2 | T3 | T1 | T2 | T3 |
квант время -->
^
здесь — переключение контекста
(сохранили T1, загрузили T2)Что происходит при переключении
В момент переключения ядро должно:
- сохранить регистры и счётчик команд текущего потока в его управляющую структуру;
- выбрать следующий поток по политике планирования;
- загрузить его сохранённое состояние;
- обновить указатель стека.
Это не бесплатно. Помимо самих сохранений, страдают кэши процессора: новый поток работает с другими данными, и кэш «остывает». Поэтому слишком частые переключения снижают производительность.
threads = ["T1", "T2", "T3"]
quantum = 0
# моделируем round-robin: по кругу выдаём кванты
for _ in range(6):
current = threads[quantum % len(threads)]
print(f"квант {quantum}: выполняется {current}")
quantum += 1Вывод:
квант 0: выполняется T1 квант 1: выполняется T2 квант 2: выполняется T3 квант 3: выполняется T1 квант 4: выполняется T2 квант 5: выполняется T3
Как работает под капотом
Планировщик хранит потоки в очередях по состояниям: «готов к выполнению», «выполняется», «заблокирован» (ждёт I/O или блокировку). Когда поток просит данные с диска, он переходит в «заблокирован» и освобождает ядро, не дожидаясь конца кванта — это добровольная уступка. Когда данные пришли, поток возвращается в очередь «готов». Политики бывают разные: round-robin, приоритетные очереди, более сложные алгоритмы вроде CFS в Linux.
Частые ошибки
- Думать, что порядок выполнения потоков предсказуем. Планировщик может переключиться в любой момент — отсюда недетерминированность гонок.
- Создавать тысячи потоков. Переключений станет так много, что процессор будет тратить время на них, а не на работу (thrashing).
- Полагаться на «успеет до переключения». Вытеснение может произойти между любыми двумя инструкциями.
Итог
- Планировщик делит ядро между потоками квантами времени.
- Вытесняющая многозадачность отбирает ядро по таймеру, не спрашивая поток.
- Переключение контекста стоит времени и «остужает» кэш.
- Момент переключения непредсказуем — это корень состояний гонки.