Потоки исполнения

Для чего Windows поддерживает потоки?

На заре компьютерной эры не существовало концепции потоков исполнения; точнее, существовал только один поток. При этом выполнение приложения могло ставить на паузу работу операционной системы.

Для решения этой проблемы было решено запускать каждый экземпляр приложения в отдельном процессе. Процессом называется набор ресурсов, используемый отдельным экземпляром приложения. Каждому процессу выделяется виртуальное адресное пространство, которое гарантирует изолированность приложений друг от друга.

Однако, если процессор один, а приложение войдёт в бесконечный цикл, то это всё так же ставит систему в тупик. Для решения этой проблемы были придуманы потоки (threads). Потоки предназначены для виртуализации процессора. Если код войдёт в бесконечный цикл, то блокируется только связанный с этим кодом процесс.

Ресурсоёмкость потоков

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

  • Объект ядра потока (thread kernel object). Операционная система выделяет и инициализирует для каждого созданного к ней потока одну из структур данных, которая описывает поток.
  • Блок окружения поток (Thread Environment Block, TEB). Место в памяти, выделенное и инициализированное в пользовательском режиме (адресное пространство, к которому приложение имеет быстрый доступ). Блок содержит заголовок цепочки обработки исключений, а также локальное хранилище данных для потока.
  • Стек пользовательского режима (user-mode stack). Хранит передаваемые в методы локальные переменные и аргументы. Также содержит адрес, показывающий, откуда начнёт исполнение поток после того, как текущий метод возвратит управление.
  • Стек режима ядра (kernel-mode stack). Используется, когда код приложения передаёт аргументы в функцию операционной системы, находящейся в режиме ядра.
  • Уведомления о создании и завершении потоков. Передаются в каждую загруженную в процесс DLL, чтобы выполнить инициализацию или очистку. Не передаются в DLL управляемых языков, так как они не имеют метода DllMain.

На самом деле ситуация с производительностью ещё хуже из-за необходимости переключения контекста (context switching). Операционная система должна распределять ресурсы физического процессора между виртуальными. В произвольный момент времени Windows передаёт процессору на исполнение один поток. Этот поток исполняется в течение некоторого временного интервала - такта (quantum). После завершения интервала происходит переключение на другой поток. При этом обязательно происходит следующее:

  1. Значения регистров процессора исполняющегося в данный момент потока сохраняются в структуре контекста, которая располагается в ядре потока.
  2. Из набора потоков выделяется тот, которому будет передано управление. Если выбранный поток принадлежит другому процессу, Windows переключает для процессора виртуальное адресное пространство. Только после этого возможно выполнение кода или доступ к данным.
  3. Значения выбранной структуры контекста потока загружаются в регистры процессора.

После переключения контекста процессор исполняет выбранный поток, пока не истечёт выделенное потоку время, после чего снова происходит переключение. Windows делает это примерно каждые 30 мс. Никакого выигрыша в производительности или потреблении памяти это не даёт, это требуется для повышения отказоустойчивости, чтобы система могла реагировать на действия конечного пользователя.

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

Иногда в конце такта Windows может продолжить исполнение уже исполняемого потока, при этом переключение контекста не переходит, а быстродействие повышается. Аналогично быстродействие повышается, если поток уступает управление до завершения такта, например, засыпая в ожидании ввода-вывода.

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

Так дальше не пойдёт!

Разработчики Windows отдали предпочтение надёжности, поэтому многие приложения создают потоки, вместо процессов, так как создание процессов - дорогостоящая процедура. Однако в результате создаётся множество простаивающих потоков.

Тенденция развития процессоров

Существует три вида многопроцессорных технологий:

  • Многопроцессорные решения. Популярность сходит на нет из-за большого размера и высокой стоимости.
  • Гиперпотоковые микросхемы. Технология Intel, которая позволяет одной микросхеме функционировать как две. Для ОС это выглядит как наличие двух процессоров, и она одновременно планирует поведение двух потоков, выполняя только один из них.
  • Многоядерные микросхемы.

CLR- и Windows-потоки

CLR-потоки аналогичны потокам Windows.

Потоки для асинхронных вычислительных операций

По возможности для этой цели лучше прибегать к доступному в CLR пулу потоков (thread pool). Однако иногда возможны ситуации, когда явно требуется создать поток для выполнения конкретной вычислительной операции, например, при выполнении кода, приводящего поток в отличное от обычного состояния потока из пула:

  • Требуется запустить поток с нестандартным приоритетом.
  • Требуется, чтобы поток выполнялся в фоновом режиме, чтобы приложение не закрылось до завершения потоком задания.
  • Может возникнуть необходимость преждевременно завершить исполняющий поток.

Причины использования потоков

Потоки используются по двум основным причинам:

  • Улучшение времени отклика (обычно для клиентских приложений с графическим интерфейсом). Это может быть реализовано как для выделения приложения в отдельный поток, так и выделения части приложения в отдельный поток.
  • Производительность (для клиентских и серверных приложений).

Планирование и приоритеты потоков

После каждого такта Windows просматривает все существующие ядра потоков в поисках потоков, которые не находятся в режиме ожидания, выбирает один из них и переключается на его контекст.

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

Каждому потоку назначается уровень приоритета с нулевого (самого низкого) до 31 (самого высокого). При выборе потока, который будет передан процессору, сначала рассматриваются потоки с самым высоким приоритетом и ставятся в очередь в цикле. При наличии в очереди потоков с приоритетом 31 система никогда не передаст процессору поток с меньшим приоритетом. Это условие называется зависанием (starvation). Зависание реже возникает на многопроцессорных машинах, так как они могут одновременно выполнять потоки с приоритетом 30 и 31. В процессе загрузки система создаёт поток обнуления страниц (zero page thread), которому назначается нулевой приоритет.

В Windows существует абстрактная прослойка над уровнями приоритетов. Поддерживается шесть классов приоритетов: Idle (холостого хода), Below Normal (ниже обычного), Normal (обычный), Above Normal (выше обычного), High (высокий) и Realtime (реального времени). Кроме того, поддерживается семь относительных приоритетов потоков: Idle (холостого хода), Lowest (самый низкий), Below Normal (ниже обычного), Normal (обычный), Above Normal (выше обычного), Highest (самый высокий) и Time-Critical (требующий немедленной обработки). Соотношений между классом приоритета, относительным приоритетом потока и итоговым уровнем приоритета можно посмотреть в книге.

Фоновые и активные потоки

При завершении активных (foreground) потоков в процессе CLR принудительно завершает также все запушенные на этот момент фоновые (background) потоки.

Что дальше?

...