Что такое поток windows

Процессы Windows

Понятие «процесса» существовало в операционных системах Windows задолго до появления платформы .NET. Попросту говоря, под процессом понимается выполняющаяся программа. Однако формально — это концепция уровня операционной системы, которая используется для описания набора ресурсов (таких как внешние библиотеки кода и главный поток) и необходимой памяти, используемой выполняющимся приложением. Для каждого загружаемого в память файла *.ехе в операционной системе создается отдельный изолированный процесс, который используется на протяжении всего времени его существования. Благодаря такой изоляции приложений, исполняющая среда получается гораздо более надежной и стабильной, поскольку выход из строя одного процесса никак не сказывается на работе других процессов.

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

Каждый процесс Windows получает уникальный идентификатор процесса (Process ID — PID) и может независимо загружаться и выгружаться операционной системой (в том числе программно). Как уже наверняка известно, в окне WindowsTask Manager (Диспетчер задач) имеется вкладка Processes (Процессы), на которой можно просматривать различные статические данные о выполняющихся на данной машине процессах, в том числе их PID-идентификаторы и имена образов. Чтобы открыть окно диспетчера задач, нажмите комбинацию клавиш :

Роль потоков

В каждом процессе Windows содержится первоначальный «поток», который является входной точкой для приложения. Потоком называется используемый внутри процесса путь выполнения. Формально поток, который создается первым во входной точке процесса, называется главным потоком (primary thread). В любой исполняемой программе .NET (консольном приложении, приложении Windows Forms, приложении WPF и т.д.) входная точка обозначается как метод Main(). При вызове этого метода главный поток создается автоматически.

Процессы, в которых содержится единственный главный поток выполнения, изначально являются безопасными к потокам (thread safe), поскольку в каждый отдельный момент времени доступ к данным приложения в них может получать только один поток. Однако подобные однопоточные процессы (особенно с графическим пользовательским интерфейсом) часто замедленно реагируют на действия пользователя, когда их единственный поток выполняет какую-то сложную операцию (вроде вывода на печать длинного текстового файла, сложных математических вычислений или подключения к удаленному серверу).

Из-за такого потенциального недостатка однопоточных приложений, API-интерфейс Windows (а также платформа .NET) предоставляет возможность для главного потока порождать дополнительные вторичные потоки (также называемые рабочими потоками). Это делается с применением набора функций из API-интерфейса Windows, таких как CreateThread() . Каждый поток (первичный или вторичный) в процессе становится уникальным путем выполнения и может параллельно получать доступ ко всем разделяемым элементам данных внутри соответствующего процесса.

Как нетрудно догадаться, разработчики обычно создают дополнительные потоки для улучшения общей степени восприимчивости программы к действиям пользователя. Многопоточные процессы обеспечивают иллюзию того, что выполнение многочисленных действий происходит примерно в одно и то же время. Например, дополнительный рабочий поток может порождаться в приложении для выполнения какой-нибудь трудоемкой задачи (подобной выводу на печать большого текстового файла). После начала выполнения задачи вторичным потоком основной поток все равно не утрачивает способности реагировать на действия пользователя, что дает всему процессу возможность сопровождаться куда более высокой производительностью.

Однако такого может и не происходить: в случае использования слишком большого количества потоков в одном процессе его производительность может даже ухудшаться из-за возникновения у ЦП необходимости переключаться между активными потоками в процессе (что отнимает определенное время).

Читайте также:  Connecting to from with linux

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

Чтобы поток не забывал, на чем он работал перед тем, как его выполнение было приостановлено, каждому потоку предоставляется возможность записывать данные в и выделяется отдельный стек вызовов, как показано на рисунке:

Системное программирование в Windows

КОМПЬЮТЕРНЫЕ КУРСЫ «ПОИСК»

1. Потоки и процессы

1.1. Определение потока

Потоком в Windows называется объект ядра, которому операционная система выделяет процессорное время для выполнения приложения. Каждому потоку принадлежат следующие ресурсы:

  • код исполняемой функции;
  • набор регистров процессора;
  • стек для работы приложения;
  • стек для работы операционной системы;
  • маркер доступа, который содержит информацию для системы безопасности.

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

В операционных системах Windows различаются потоки двух типов:

  • системные потоки;
  • пользовательские потоки.

Системные потоки выполняют различные сервисы операционной системы и запускаются ядром операционной системы.

Пользовательские потоки служат для решения задач пользователя и запускаются приложением.

В работающем приложении различаются потоки двух типов:

  • рабочие потоки (working threads);
  • потоки интерфейса пользователя (user interface threads).

Рабочие потоки выполняют различные фоновые задачи в приложении. Потоки интерфейса пользователя связаны с окнами и выполняют обработку сообщений, поступающих этим окнам. Каждое приложение имеет, по крайней мере, один поток, который называется первичным (primary) или главным (main) потоком. В консольных приложениях это поток, который исполняет функцию main. В приложениях с графическим интерфейсом это поток, который исполняет функцию WinMain.

Создается поток функцией CreateThread, которая имеет следующий прототип:

function CreateThread(
lpThreadAttributes: Pointer; // атрибуты защиты
dwStackSize: DWORD; // размер стека потока в байтах
lpStartAddress: TFNThreadStartRoutine; // адрес функции
lpParameter: Pointer; // адрес параметра
dwCreationFlags: DWORD; // флаги создания потока
var lpThreadId: DWORD // идентификатор потока
): THandle;

При успешном завершении функция CreateThread возвращает дескриптор созданного потока и его идентификатор, который является уникальным для всей системы. В противном случае эта функция возвращает значение nil.

Назначение параметров

Параметр lpThreadAttributes устанавливает атрибуты защиты создаваемого потока. До тех пор пока мы не изучим систему безопасности в Windows, мы будем устанавливать значения этого параметра в nil при вызове почти всех функций ядра Windows. В данном случае это означает, что операционная система сама установит атрибуты защиты потока, используя настройки по умолчанию.

Параметр dwStacksize определяет размер стека, который выделяется потоку при запуске. Если этот параметр равен нулю, то потоку выделяется стек, размер которого по умолчанию равен 1 Мбайт. Это наименьший размер стека, который может быть выделен потоку. Если величина параметра dwStacksize меньше значения, заданного по умолчанию, то все равно потоку выделяется стек размером в 1 Мбайт. Операционная система Windows округляет размер стека до одной страницы памяти, который обычно равен 4 Кбайт.

Параметр lpStartAddress указывает на исполняемую потоком функцию.

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

Параметр dwCreationFiags определяет, в каком состоянии будет создан поток. Если значение этого параметра равно 0, то функция потока начинает выполняться сразу после создания потока. Если же значение этого параметра равно CREATE_SUSPENDED, то поток создается в подвешенном состоянии. В дальнейшем этот поток можно запустить вызовом функции ResumeThread.

Параметр lpThreadId является выходным, т. е. его значение устанавливает Windows. Этот параметр должен указывать на переменную, в которую Windows поместит идентификатор потока. Этот идентификатор уникален для всей системы и может в дальнейшем использоваться для ссылок на поток. Идентификатор потока главным образом используется системными функциями и редко функциями приложения. Действителен идентификатор потока только на время существования потока. После завершения потока тот же идентификатор может быть присвоен другому потоку.

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

В листинге 1.1 приведен пример программы, которая использует функцию CreateThread для создания потока и демонстрирует способ передачи параметров исполняемой потоком функции.

Листинг 1.1. Создание потока функцией CreateThread

Отметим, что в этой программе используется функция WaitForSingleObject, которая ждет завершения потока Add.

Поток завершается вызовом функции ExitThread, которая имеет следующий прототип:

procedure ExitThread(
dwExitCode: DWORD //код завершения потока
); stdcall;

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

Один поток может завершить другой поток, вызвав функцию TerminateThread, которая имеет следующий прототип:

function TerminateThread(
hThread: THandle; //дескриптор потока
dwExitCode: DWORD; //код завершения потока
): BOOL; stdcall;

В случае успешного завершения функция TerminateThread возвращает ненулевое значение, в противном случае — FALSE. Функция TerminateThread завершает поток, но не освобождает все ресурсы, принадлежащие этому потоку. Это происходит потому, что при выполнении этой функции система не посылает динамическим библиотекам, загруженным процессом, сообщение о том, что поток завершает свою работу. В результате динамическая библиотека не освобождает ресурсы, которые были захвачены для работы с этим потоком. Поэтому эта функция должна вызываться только в аварийных ситуациях при зависании потока.

В листинге 1.2 приведена программа, которая демонстрирует работу функции TerminateThread.

Каждый созданный поток имеет счетчик приостановок, максимальное значение которого равно MAXIMUM_SUSPEND_COUNT. Счетчик приостановок показывает, сколько раз исполнение потока было приостановлено. Поток может исполняться только при условии, что значение счетчика приостановок равно нулю. В противном случае поток не исполняется или, как говорят, находится в подвешенном состоянии. Исполнение каждого потока может быть приостановлено вызовом функции SuspendThread, которая имеет следующий прототип:

function SuspendThread(
hThread: THandle //дескриптор потока
): DWORD; stdcall;

Эта функция увеличивает значение счетчика приостановок на 1 и, при успешном завершении, возвращает текущее значение этого счетчика. В случае неудачи функция SuspendThread возвращает значение, равное -1.

Отметим, что поток может приостановить также и сам себя. Для этого он должен передать функции SuspendThread свой псевдодескриптор, который можно получить при помощи функции GetCurrentThread.

Для возобновления исполнения потока используется функция ResumeThread, которая имеет следующий прототип:

function ResumeThread(
hThread: THandle //дескриптор потока
): DWORD; stdcall;

Функция ResumeThread уменьшает значение счетчика приостановок на 1 при условии, что это значение было больше нуля. Если полученное значение счетчика приостановок равно 0, то исполнение потока возобновляется, в противном случае поток остается в подвешенном состоянии. Если при вызове функции ResumeThread значение счетчика приостановок было равным 0, то это значит, что поток не находится в подвешенном состоянии. В этом случае функция не выполняет никаких действий. При успешном завершении функция ResumeThread возвращает текущее значение счетчика приостановок, в противном случае — значение -1.

Поток может задержать свое исполнение вызовом функции Sleep, которая имеет следующий прототип:

procedure Sleep(
dwMilliseconds: DWORD //миллисекунды
); stdcall;

Единственный параметр функции Sleep определяет количество миллисекунд, на которые поток, вызвавший эту функцию, приостанавливает свое исполнение. Если значение этого параметра равно 0, то выполнение потока просто прерывается, а затем возобновляется при условии, что нет других потоков, ждущих выделения процессорного времени. Если же значение этого параметра равно INFINITE, тo поток приостанавливает свое исполнение навсегда, что приводит к блокированию работы приложения.

В листинге 1.3 приведена программа, которая демонстрирует работу функций SuspendThread, ResumeThread и Sleep.

Иногда потоку требуется знать свой дескриптор, чтобы изменить какие-то свои характеристики. Например, поток может изменить свой приоритет. Для этих целей в Win32 API существует функция GetcurrentThread, которая имеет следующий прототип:

function GetCurrentThread: THandle; stdcall;

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

В листинге 1.4 приведен пример программы, которая вызывает функцию GetCurrentThread, а затем выводит на консоль полученный псевдодескриптор.

Большинство функций Win32 API возвращают код, по которому можно определить, как завершилась функция: успешно или нет. Если функция завершилась неудачей, то код возврата обычно равен false, nil или -1. В этом случае функция Win32 API также устанавливает внутренний код ошибки, который называется кодом последней ошибки (last-error code) и поддерживается отдельно для каждого потока. Чтобы получить код последней ошибки, нужно вызвать функцию GetLastError, которая имеет следующий прототип:

function GetLastError: DWORD; stdcall;

Эта функция возвращает код последней ошибки, установленной в потоке. Установить код последней ошибки в потоке можно при помощи функции SetLastError, имеющей следующий прототип:

procedure SetLastError(
dwErrCode: DWORD //код ошибки
); stdcall;

Чтобы получить сообщение, соответствующее коду последней ошибки, необходимо использовать функцию FormatMessage, которая имеет следующий прототип:

function FormatMessage(
dwFlags: DWORD; // режимы форматирования
lpSource: Pointer; // источник сообщения
dwMessageId: DWORD; // идентификатор сообщения
dwLanguageId: DWORD; // идентификатор языка
lpBuffer: PChar; // буфер для сообщения
nSize: DWORD; // максимальный размер буфера для сообщения
Arguments: Pointer // список значений для вставки в сообщение
): DWORD; stdcall;

В листинге 1.5 приведен пример программы, которая вызывает функцию FormatMessage

Исходный код скачать. Выполнен на Delphi XE.

Используемая литература: Александр Побегайло «Системное программироввние в Windows»

Читайте также:  Смещен рабочий стол windows 10
Оцените статью