20
0
0
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
Назад

Многопоточность. Снизу вверх. ОС

Время чтения 7 минут
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
20
0
0
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники

Всем привет! Меня зовут Дмитрий Бахтенков. Добро пожаловать во вторую часть цикла статей «Многопоточность. Снизу вверх». Сегодня расскажу о реализации многопоточности на уровне операционных систем.

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

Многопоточность. Снизу вверх. ОС

Многозадачность

Многозадачность — это свойство операционной системы обеспечивать возможность параллельной обработки нескольких задач. Многозадачность может быть двух видов:

  • Процессная. Задачи выполняются в одно время, но в разных процессах операционной системы. Например, когда вы работаете в текстовом редакторе и слушаете музыку на ПК.
  • Поточная. Задачи выполняются одновременно в рамках одной программы.

В этой статье рассмотрим именно поточную многозадачность.

Процесс и поток

Что происходит, когда мы запускаем какое-то приложение?
Операционная система создает процесс, который получает:

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

Если процесс порождает другие процессы, они становятся дочерними.

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

Создание потока «дешевле» для ОС, чем создание процесса, при этом потоки могут обмениваться данными друг с другом через общее адресное пространство. Процессам для этого нужен отдельный механизм межпроцессного взаимодействия.

Как ОС создает поток:

  • Поток получает общий контекст процесса, в рамках которого выделяется память, создаются файловые дескрипторы, сетевые соединения.
  • Для потока выделяется отдельный участок памяти для его стека. Стек нужен для локальных переменных и вызовов функций внутри потока.
  • ОС создает 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. Там я рассказываю о своем опыте в программировании, делаю обзоры книг, делюсь интересными новостями и необычными техническими кейсами.

Комментарии0
Тоже интересно
Комментировать
Поделиться
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники