Потоки исполнения
Для чего Windows поддерживает потоки?
На заре компьютерной эры не существовало концепции потоков исполнения; точнее, существовал только один поток. При этом выполнение приложения могло ставить на паузу работу операционной системы.
Для решения этой проблемы было решено запускать каждый экземпляр приложения в отдельном процессе. Процессом называется набор ресурсов, используемый отдельным экземпляром приложения. Каждому процессу выделяется виртуальное адресное пространство, которое гарантирует изолированность приложений друг от друга.
Однако, если процессор один, а приложение войдёт в бесконечный цикл, то это всё так же ставит систему в тупик. Для решения этой проблемы были придуманы потоки (threads). Потоки предназначены для виртуализации процессора. Если код войдёт в бесконечный цикл, то блокируется только связанный с этим кодом процесс.
Ресурсоёмкость потоков
Как и любые механизмы виртуализации, потоки потребляют дополнительные ресурсы, требуя памяти и времени (снижая производительность среды выполнения). Каждый поток состоит из нескольких частей:
- Объект ядра потока (thread kernel object). Операционная система выделяет и инициализирует для каждого созданного к ней потока одну из структур данных, которая описывает поток.
- Блок окружения поток (Thread Environment Block, TEB). Место в памяти, выделенное и инициализированное в пользовательском режиме (адресное пространство, к которому приложение имеет быстрый доступ). Блок содержит заголовок цепочки обработки исключений, а также локальное хранилище данных для потока.
- Стек пользовательского режима (user-mode stack). Хранит передаваемые в методы локальные переменные и аргументы. Также содержит адрес, показывающий, откуда начнёт исполнение поток после того, как текущий метод возвратит управление.
- Стек режима ядра (kernel-mode stack). Используется, когда код приложения передаёт аргументы в функцию операционной системы, находящейся в режиме ядра.
- Уведомления о создании и завершении потоков. Передаются в каждую загруженную в процесс DLL, чтобы выполнить инициализацию или очистку. Не передаются в DLL управляемых языков, так как они не имеют метода DllMain.
На самом деле ситуация с производительностью ещё хуже из-за необходимости переключения контекста (context switching). Операционная система должна распределять ресурсы физического процессора между виртуальными. В произвольный момент времени Windows передаёт процессору на исполнение один поток. Этот поток исполняется в течение некоторого временного интервала - такта (quantum). После завершения интервала происходит переключение на другой поток. При этом обязательно происходит следующее:
- Значения регистров процессора исполняющегося в данный момент потока сохраняются в структуре контекста, которая располагается в ядре потока.
- Из набора потоков выделяется тот, которому будет передано управление. Если выбранный поток принадлежит другому процессу, Windows переключает для процессора виртуальное адресное пространство. Только после этого возможно выполнение кода или доступ к данным.
- Значения выбранной структуры контекста потока загружаются в регистры процессора.
После переключения контекста процессор исполняет выбранный поток, пока не истечёт выделенное потоку время, после чего снова происходит переключение. 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) потоки.
Что дальше?
...
