Многозадачность
Многозадачность — это свойство операционной системы обеспечивать возможность параллельной обработки нескольких задач. Многозадачность может быть двух видов:
- Процессная. Задачи выполняются в одно время, но в разных процессах операционной системы. Например, когда вы работаете в текстовом редакторе и слушаете музыку на ПК.
- Поточная. Задачи выполняются одновременно в рамках одной программы.
В этой статье рассмотрим именно поточную многозадачность.
Процесс и поток
Что происходит, когда мы запускаем какое-то приложение?
Операционная система создает процесс, который получает:
- адресное пространство — оперативную память, где он может работать;
- первичный поток, в рамках которого этот процесс выполняется;
- доступ к файловой системе (при необходимости).
Если процесс порождает другие процессы, они становятся дочерними.

Каждый процесс выполняется в своем потоке, поэтому задачи можно выполнять параллельно с помощью разных процессов. Однако выделять на каждую задачу свое адресное пространство, создавать поток, давать доступ к файловой системе довольно затратно по ресурсам системы и времени. Именно поэтому операционные системы позволяют создавать множество потоков в рамках одного процесса.

Создание потока «дешевле» для ОС, чем создание процесса, при этом потоки могут обмениваться данными друг с другом через общее адресное пространство. Процессам для этого нужен отдельный механизм межпроцессного взаимодействия.
Как ОС создает поток:
- Поток получает общий контекст процесса, в рамках которого выделяется память, создаются файловые дескрипторы, сетевые соединения.
- Для потока выделяется отдельный участок памяти для его стека. Стек нужен для локальных переменных и вызовов функций внутри потока.
- ОС создает Thread Control Block — специальную структуру данных, где хранится информация о потоке: его уникальный идентификатор, указатели на стек, статус потока (запущен, в ожидании и т. д.).
- Поток регистрируется у планировщика задач ОС, чтобы тот запустил его на свободном ядре или поставил в очередь ожидания.
- Поток выполняется.
Потоки и ядра в операционных системах
Операционная система назначает потоки на ядра, где они затем выполняются. Это может быть непосредственно ядро процессора (физическое), они бывают разных типов даже в одном устройстве. А также ядро может быть логическое, то есть иметь возможность выполнять потоки параллельно.

Количество доступных ядер для распределения на них потоков можно посмотреть в диспетчере задач Windows:

У этого процессора шесть физических ядер с двумя потоками и восемь физических ядер с одним потоком. 6 × 2 + 8 = 20 — таким образом, мы имеем 20 ядер.
Доступ к общим данным
Я уже упоминал, что потоки могут обмениваться данными через общую память. Из-за этого могут возникать проблемы с доступом к данным:
- Состояние гонки — ошибка проектирования многопоточной системы или приложения, при которой работа системы или приложения зависит от того, в каком порядке выполняются части кода.
Например, поток A изменяет данные, а поток B проверяет их на валидность. Если сначала выполняется A, а потом B, логика верна и всё работает правильно. Но если B выполнится раньше, то мы получим некорректные данные.
- Взаимоблокировка — ситуация, при которой потоки ожидают заблокированные друг другом ресурсы.
Представьте, что вы идете по дороге навстречу другому человеку. Чтобы обойти друг друга, вы двигаетесь в одну и ту же сторону и из-за этого сталкиваетесь. Это пример взаимной блокировки.
Чтобы избежать этих проблем, существуют примитивы синхронизации потоков. Разберем некоторые из них.
Lock (блокировка)
Это примитив синхронизации, который используется для доступа потоков к разделяемым ресурсам. По сути, это флаг в памяти, который говорит, занят ли сейчас ресурс. Если ресурс свободен, поток блокирует его и устанавливает флаг в состояние «занят». Остальные потоки встают в очередь. Lock виден только в рамках одного процесса.

Mutex (мьютекс)
Mutex — такой же флаг, что и Lock, но глобальный: его видят все процессы операционной системы.

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

Планировщик в ОС
Планировщик управляет порядком, в котором процессы и потоки могут получить доступ к центральному процессору. В контексте многопоточности именно планировщик определяет, в какой момент, на каком ядре и с каким набором ресурсов будет запущен поток.
Алгоритмы планирования
Планировщик может использовать различные алгоритмы для справедливого распределения ресурсов:
- Очередь — выполняем потоки в порядке их поступления.

Проблема очереди в том, что задачи с высоким приоритетом могут «зависать» в ожидании и долго не исполняться.
- Round Robin — каждому потоку выделяется некоторый квант времени, и все они выполняются по кругу.

Проблема: при слишком маленьком кванте происходит частое переключение между потоками, а при слишком большом важные задачи могут долго не выполняться.
- Алгоритмы с приоритетом. В современных операционных системах чаще всего используется комбинированная реализация планировщика. Например, базово потоки используют очередь с приоритетом, а при одинаковом приоритете — Round Robin.
Реализации планировщиков в ОС: Linux CFS и Windows Scheduler.
Переключение контекста
На каждом логическом ядре в конкретный момент выполняется только один поток. Если их больше, чем ядер, то нужно уметь переключаться между потоками в зависимости от приоритетов и других условий, чтобы ядро не простаивало.
Например, когда поток переходит в состояние ожидания из-за блокировки, ОС можно сделать активным другой поток — переключить контекст.
Чем больше потоков выполняется, тем чаще ОС приходится переключаться между ними. На это уходит много ресурсов:
- процессорное время тратится на сохранение и загрузку данных;
- кеши работают хуже: текущий поток может изменить данные, и их придется повторно загружать для нового потока;
- доступ к памяти замедляется, если поток использует большие объемы данных.
Этапы переключения контекста
1. Сохранение текущего состояния:
- Регистры процессора (счетчик команд, указатели на стек, флаги состояния).
- Данные стека (локальные переменные и адрес возврата).
- Статус потока (выполняется, готов, ожидает).
- Кешированные данные и данные, связанные с виртуальной памятью.
2. Выбор нового потока:
- Планировщик выбирает поток в зависимости от алгоритма планирования (FIFO, Round Robin, Priority Scheduling и др.).
- Если есть потоки с одинаковым приоритетом, выбор может быть случайным или по порядку.
3. Загрузка состояния нового потока:
- Восстанавливаются регистры и указатели стека.
- Переключение на новый контекст виртуальной памяти (при смене процесса).
- Загружаются данные из кешей или оперативной памяти, если они недоступны.
Заключение
В этой статье я описал основные аспекты того, как работают потоки в операционной системе: разобрал понятие многозадачности, осветил назначение потоков на ядра и примитивы синхронизации, объяснил алгоритмы планировщика задач в ОС и переключение контекста. Спасибо за прочтение!
Заглядывайте ко мне в телеграм-канал Flexible Coding. Там я рассказываю о своем опыте в программировании, делаю обзоры книг, делюсь интересными новостями и необычными техническими кейсами.