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

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

[Главная страница] [Системное программирование] [Контакты]

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


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

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

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

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

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

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

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

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

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

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

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

1.2. Создание потоков

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

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

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

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

lpThreadAttributes

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

dwStackSize

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

lpStartAddress

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

lpParameter

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

dwCreationFlags

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

lpThreadId

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

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

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

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


program CreateThreadd;

{$APPTYPE CONSOLE}

uses
  SysUtils,
  Windows;

var
  n: Integer = 0;
  inc: Integer = 10;
  hThread: HWND;
  IDThread: DWORD;

procedure Add(iNum: Pointer); stdcall;
begin
  Writeln('Thread is started');
  n := n + Integer(iNum^);
  Writeln('Thread is finished');
end;

begin
  Writeln('n = ', n);
  //запускаем поток Add
  hThread := CreateThread(nil, 0, @Add, @inc, 0, IDThread);
  //Ждем, пока поток Add закончит работу
  WaitForSingleObject(hThread, INFINITE);
  //закрываем дескриптор потока Add
  CloseHandle(hThread);
  Writeln('n = ', n);
  Readln;
end.

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

1.3. Завершение потоков

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

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

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

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

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

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

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


program TerminateThreadd;

{$APPTYPE CONSOLE}

uses
  SysUtils, Windows;

var
  count: Cardinal = 0;
  hThread: HWND;
  IDThread: DWORD;
  c: Char;
  b1: Boolean = True;

procedure thread; stdcall;
var
  b2: Boolean;
begin
  b2 := True;
  while b2 do
  begin
    count := count + 1;
    Sleep(100); // немного отдохнем
  end;
end;

begin
  hThread := CreateThread(nil, 0, @thread, nil, 0, IDThread);
  while b1 do
  begin
    Write('Input ''y'' to display the count or any char to finish: ');
    Readln(c);
    if c = 'y' then
      Writeln('count = ', count)
    else
      Break;
  end;

  //прерываем выполнение потока thread
  TerminateThread(hThread, 0);

  //закрываем дескриптор потока
  CloseHandle(hThread);
end.

1.4. Приостановка и возобновление потоков

Каждый созданный поток имеет счетчик приостановок, максимальное значение которого равно 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.


//Пример работы функций SuspendThread, ResumeThread и Sleep
program SuspendThreadd;

{$APPTYPE CONSOLE}

uses
  SysUtils, Windows;

var
  nCount: Cardinal = 0;
  dwCount: DWORD;
  hThread: HWND;
  IDThread: DWORD;
  c: Char;
  b: Boolean = True;

procedure thread; stdcall;
begin
  while b do
  begin
    nCount := nCount + 1;
    Sleep(100); // приостанавливаем поток на 100 миллисекунд
  end;
end;

begin
  hThread := CreateThread(nil, 0, @thread, nil, 0, IDThread);
  while b do
  begin
    Writeln('Input :');
    Writeln(#9, '''n'' to exit');
    Writeln(#9, '''y'' to display the count');
    Writeln(#9, '''s'' to suspend thread');
    Writeln(#9, '''r'' to resume thread');

    Readln(c);

    case c of
      'n': Break;
      'y': Writeln('count = ', nCount);
      's':
        begin
          //приостанавливаем поток thread
          dwCount :=  SuspendThread(hThread);
          Writeln('Thread suspend count = ', dwCount);
        end;
      'r':
        begin
          //возобнавляем поток thread
          dwCount := ResumeThread(hThread);
          Writeln('Thread suspend count = ', dwCount);
        end;
    end;
  end;

  //прерываем выполнение потока thread
  TerminateThread(hThread, 0);

  //закрываем дескриптор потока
  CloseHandle(hThread);
end.

1.5. Псевдодескрипторы потоков

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

function GetCurrentThread: THandle; stdcall;

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

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


//Пример работы функции GetcurrentThread
program GetCurrentThreadd;

{$APPTYPE CONSOLE}

uses
  SysUtils, Windows;

var
  hThread: HWND;

begin
  // получаем псевдодескриптор текущего потока 
  hThread := GetCurrentThread;
  // получаем псевдодескриптор текущего потока
  Writeln(hThread);
  Readln;
end.

1.6. Обработка ошибок в Windows

Большинство функций 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


program ErrorMessageBoxx;

{$APPTYPE CONSOLE}

uses
  SysUtils, Windows;

var
  hHandle: THandle;

procedure ErrorMessageBox;
var
  lpMsgBuf: PChar;
begin
  FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER or FORMAT_MESSAGE_FROM_SYSTEM
                  or FORMAT_MESSAGE_IGNORE_INSERTS, nil, GetLastError, 0,
                  @lpMsgBuf, 0, nil);
  MessageBox(0, lpMsgBuf, 'Error Win32 API', MB_OK or MB_ICONINFORMATION);

  //Освободить буфер
  LocalFree(Integer(lpMsgBuf));
end;

//тест для функции вывода сообщения об ошибке на консоль
begin
  hHandle := 0;
  //неправильный вызов функции закрытия дескритптора
  if not CloseHandle(hHandle) then
    ErrorMessageBox;
end.

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

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