Как ОС планирует потоки и переключает контекст

Откуда берётся «одновременность» на одном ядре: планировщик и переключение контекста.

Контекстное переключение — это сохранение состояния текущего потока (регистров, счётчика команд) и загрузка состояния другого, чтобы процессор продолжил выполнять уже его.

Потоков обычно гораздо больше, чем ядер. Чтобы все они продвигались, операционная система применяет планировщик: он выдаёт каждому потоку небольшой отрезок процессорного времени — квант (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).
  • Полагаться на «успеет до переключения». Вытеснение может произойти между любыми двумя инструкциями.

Итог

  • Планировщик делит ядро между потоками квантами времени.
  • Вытесняющая многозадачность отбирает ядро по таймеру, не спрашивая поток.
  • Переключение контекста стоит времени и «остужает» кэш.
  • Момент переключения непредсказуем — это корень состояний гонки.
Проверьте себя
1. Что такое контекстное переключение?
AЗапуск нового процесса с нуля
BСохранение состояния одного потока и загрузка состояния другого
CУдаление потока из памяти
DПеренос потока на другой компьютер
2. Что характерно для вытесняющей (preemptive) многозадачности?
AПоток сам решает, когда уступить ядро
BОС может отобрать ядро по таймеру, не спрашивая поток
CПереключений вообще не бывает
DРаботает только на одном ядре
3. Почему слишком частые переключения контекста вредны?
AОни увеличивают объём памяти процесса
BПроцессор тратит время на сами переключения и теряет горячий кэш
CОни удаляют данные с диска
DОни отключают другие ядра