Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

лекции / Shchupak_Yu._Win32_API_Razrabotka_prilozheniy_dlya_Windows

.pdf
Скачиваний:
0
Добавлен:
11.02.2026
Размер:
13.15 Mб
Скачать

Управление потоками

451

 

 

После запуска приложения CreateMyProcess пользователь может вызвать при" ложение Калькулятор, выполнив команду меню Create process. После этого оба при" ложения будут работать независимо друг от друга. Вы можете пользоваться по" переменно и тем и другим приложением, а также в любой последовательности прекращать их выполнение.

Управление потоками

Каждый поток начинает выполнение с некоторой входной функции. В первич" ном потоке используется одна из функций: WinMain, wWinMain, main или wmain. Если вы хотите создать вторичный поток, в нем тоже должна быть входная функция, которая выглядит примерно так:

DWORD WINAPI ThreadFunc(PVOID pvParam) { DWORD dwResult = 0;

. . .

return dwResult;

}

Функция потока может выполнять любые задачи. Рано или поздно она закон" чит свою работу и вернет управление. В этот момент поток остановится, память, отведенная под его стек, будет освобождена, а счетчик пользователей его объекта ядра «поток» уменьшится на единицу. Когда счетчик обнулится, этот объект ядра будет разрушен.

В отличие от входной функции первичного потока, имеющей одно из предоп" ределенных имен: WinMain, wWinMain, main или wmain, — функции других потоков могут иметь произвольные имена.

Функция CreateThread

Для создания потока используется функция CreateThread:

HANDLE CreateThread(

LPSECURITY_ATTRIBUTES threadAttributes, // атрибуты доступа

DWORD dwStackSize,

// размер стека

LPTHREAD_START_ROUTINE lpStartAddress,

// адрес функции потока

LPVOID lpParameter,

// параметр функции потока

DWORD dwCreationFlags,

// флаги потока

LPDWORD lpThreadId

// идентификатор потока

);

 

Как и при работе с функцией CreateProcess, для многих параметров можно за" давать значения по умолчанию (0 или NULL). Третий параметр не может иметь значение по умолчанию, ему всегда передается адрес функции потока. Четвертый параметр часто используется для организации взаимосвязи вызывающего потока с дочерним потоком (листинг 9.2).

При каждом вызове функции CreateThread система создает объект ядра «поток» с начальным значением счетчика его пользователей, равным единице. Система вы" деляет память под стек потока из адресного пространства процесса. Новый поток выполняется в адресном пространстве того же процесса, что и родительский поток.

Завершение потока можно организовать четырьмя способами:

функция потока возвращает управление (предпочтительный способ);

452

Глава 9. Многозадачность

 

 

поток самоуничтожается вызовом функции ExitThread (рекомендация такая же, как и для функции ExitProcess);

один из потоков данного или стороннего процесса вызывает функцию Terminate Thread (нежелательный способ);

завершается процесс, содержащий данный поток (тоже нежелательно).

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

Функция Sleep

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

VOID Sleep(DWORD dwMilliseconds);

Эта функция приостанавливает выполнение потока на dwMilliseconds милли" секунд. При использовании данной функции следует учитывать несколько до" полнительных аспектов.

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

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

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

Вглаве 10 (раздел «Программирование задержек в исполнении кода») приво" дятся программные эксперименты для уточнения характеристик функции Sleep.

Пример многопоточного приложения

Приведенное ниже приложение CreateMyThreads демонстрирует создание и парал" лельную работу трех потоков.

Листинг 9.2. Проект CreateMyThreads

//////////////////////////////////////////////////////////////////////

// CreateMyThreads.cpp #include <windows.h> #include <string> using namespace std;

#include "KWnd.h"

enum UserMsg { UM_THREAD_DONE = WM_USER+1 };

 

 

struct ThreadManager {

 

 

ThreadManager(string _name) : name(_name) { nValue = 0; }

 

 

HWND hwndParent;

продолжение

 

 

Управление потоками

453

 

 

Листинг 9.2 (продолжение)

string name; int nValue;

};

ThreadManager tm_A(string("Поток A"));

ThreadManager tm_B(string("Поток B"));

ThreadManager tm_C(string("Поток C"));

DWORD WINAPI ThreadFuncA(LPVOID);

DWORD WINAPI ThreadFuncB(LPVOID);

DWORD WINAPI ThreadFuncC(LPVOID);

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); //==================================================================== int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,

LPSTR lpCmdLine, int nCmdShow)

{

MSG msg;

KWnd mainWnd("CreateMyThreads", hInstance, nCmdShow, WndProc, NULL, 100, 100, 400, 160);

while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg);

}

return (msg.wParam);

}

//==================================================================== LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)

{

HDC hDC; PAINTSTRUCT ps;

static HANDLE hThreadA, hThreadB, hThreadC; static char text[100];

ThreadManager* pTm; static int y = 0;

switch (uMsg)

{

case WM_CREATE:

tm_A.hwndParent = hWnd;

hThreadA = CreateThread(NULL, 0, ThreadFuncA, &tm_A, 0, NULL); if (!hThreadA)

MessageBox(hWnd, "Error of create hThreadA", NULL, MB_OK);

tm_B.hwndParent = hWnd;

hThreadB = CreateThread(NULL, 0, ThreadFuncB, &tm_B, 0, NULL); if (!hThreadB)

MessageBox(hWnd, "Error of create hThreadB", NULL, MB_OK);

tm_C.hwndParent = hWnd;

hThreadC = CreateThread(NULL, 0, ThreadFuncC, &tm_C, 0, NULL);

454 Глава 9. Многозадачность

if (!hThreadC)

MessageBox(hWnd, "Error of create hThreadC", NULL, MB_OK); break;

case UM_THREAD_DONE:

pTm = (ThreadManager*)wParam;

sprintf(text, "%s: count = %d", pTm->name.c_str(), pTm->nValue); y += 30;

InvalidateRect(hWnd, NULL, FALSE); break;

case WM_PAINT:

hDC = BeginPaint(hWnd, &ps); TextOut(hDC, 20, y, text, strlen(text)); EndPaint(hWnd, &ps);

break;

case WM_DESTROY:

CloseHandle(hThreadA);

CloseHandle(hThreadB);

CloseHandle(hThreadC);

PostQuitMessage(0);

break;

default:

return DefWindowProc(hWnd, uMsg, wParam, lParam);

}

return 0;

}

//==================================================================== DWORD WINAPI ThreadFuncA(LPVOID lpv)

{

ThreadManager* pTm = (ThreadManager*)lpv; int count = 0;

for (int i = 0; i < 100000000; ++i) count++; pTm->nValue = count;

SendMessage(pTm->hwndParent, UM_THREAD_DONE, (WPARAM)pTm, 0);

return 0;

}

//==================================================================== DWORD WINAPI ThreadFuncB(LPVOID lpv)

{

ThreadManager* pTm = (ThreadManager*)lpv; int count = 0;

for (int i = 0; i < 50000000; ++i) count++; pTm->nValue = count;

продолжение

Управление потоками

455

 

 

Листинг 9.2 (продолжение)

SendMessage(pTm->hwndParent, UM_THREAD_DONE, (WPARAM)pTm, 0);

return 0;

}

//==================================================================== DWORD WINAPI ThreadFuncC(LPVOID lpv)

{

ThreadManager* pTm = (ThreadManager*)lpv; int count = 0;

for (int i = 0; i < 20000; ++i) count++; pTm->nValue = count;

SendMessage(pTm->hwndParent, UM_THREAD_DONE, (WPARAM)pTm, 0);

return 0;

}

//////////////////////////////////////////////////////////////////////

Вприложении определена структура ThreadManager, объекты которой tm_A, tm_B

иtm_C используются для взаимосвязи дочерних потоков с первичным потоком. Адреса этих объектов передаются в качестве четвертого параметра при вызовах функции CreateThread.

Вблоке обработки сообщения WM_CREATE создаются три дочерних потока с дес" крипторами hThreadA, hThreadB и hThreadC.

Функции потоков ThreadFuncA, ThreadFuncB, ThreadFuncC работают по одному

итому же сценарию. В каждой из них есть локальный счетчик count, инкременти" руемый в цикле for. Разница только в числе повторений цикла: 100 000 000, 50 000 000 и 20 000 раз соответственно. После завершения цикла каждая функция посылает окну первичного потока пользовательское сообщение UM_THREAD_DONE.

Получив сообщение UM_THREAD_DONE, функция WndProc выводит в свое окно информацию о завершившемся потоке.

На рис. 9.2 показан результат запуска приложения CreateMyThreads.

Рис. 9.2. Результат выполнения программы CreateMyThreads

Хотя потоки запущены почти одновременно (порядок запуска: поток A, поток B, поток C), быстрее всех завершается поток C, затем поток B и последним — по" ток A. В этой последовательности информация о них и выводится в главное окно приложения.

В заключение стоит сделать одно замечание об использовании функции CreateThread. Дж. Рихтер предостерегает о возможных проблемах в работе прило" жения, если оно использует функции стандартной библиотеки C/C++ и создает поток вызовом функции CreateThread. В этом случае рекомендуется создавать поток

456

Глава 9. Многозадачность

 

 

вызовом функции _beginthreadex из библиотеки Visual C++. У функции _beginthreadex тот же список параметров, что и у CreateThread, но их имена и типы несколько различаются.

Чтобы упростить замену вызовов CreateThread вызовами _beginthreadex, объя" вите с помощью оператора typedef указатель на функцию

typedef unsigned (__stdcall *PTHREAD_START) (void*)

Теперь для создания потока A в рассмотренном выше приложении вы можете использовать следующую инструкцию:

hThreadA = (HANDLE)_beginthreadex(NULL, 0, (PTHREAD_START)ThreadFuncA, (void*)&tm_A, 0, NULL);

В случае применения функции _beginthreadex обязательно свяжите ваш про" ект с многопоточной версией библиотеки C/C++, иначе будут возникать ошибки компиляции. Для этого надо выполнить команду меню Project Settings и в по" явившемся диалоговом окне Project Settings перейти на вкладку C/C++. В списке

Category выберите строку Code Generation, а в списке Use run-time library — одну из многопоточных версий библиотеки, например Debug Multithreaded.

Взаимодействие потоков через глобальную переменную

Как только вы разобьете приложение на несколько потоков, вам придется решать проблемы, о существовании которых вы даже не подозревали, занимаясь програм" мированием в однопоточном режиме.

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

Листинг 9.3. Проект BadCount

//////////////////////////////////////////////////////////////////////

// BadCount.cpp #include <windows.h> #include <stdio.h> #include "KWnd.h"

#define N 50000000 long g_counter = 0;

DWORD WINAPI ThreadFunc(LPVOID); void IncCounter();

void DecCounter();

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); //==================================================================== int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,

LPSTR lpCmdLine, int nCmdShow)

{

MSG msg;

KWnd mainWnd("BadCount", hInstance, nCmdShow, WndProc,

NULL, 100, 100, 400, 100);

 

while (GetMessage(&msg, NULL, 0, 0)) {

продолжение

TranslateMessage(&msg);

 

Управление потоками

457

 

 

Листинг 9.3 (продолжение)

DispatchMessage(&msg);

}

return (msg.wParam);

}

//==================================================================== LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)

{

HDC hDC; PAINTSTRUCT ps; HANDLE hThread; char text[100];

switch (uMsg)

{

case WM_CREATE:

hThread = CreateThread(NULL, 0, ThreadFunc, NULL, 0, NULL); if (!hThread)

MessageBox(hWnd, "Error of CreateThread", NULL, MB_OK);

DecCounter();

WaitForSingleObject(hThread, INFINITE); InvalidateRect(hWnd, NULL, TRUE); break;

case WM_PAINT:

hDC = BeginPaint(hWnd, &ps);

sprintf(text, "g_counter = %d", g_counter); TextOut(hDC, 20, 20, text, strlen(text)); EndPaint(hWnd, &ps);

break;

case WM_DESTROY: PostQuitMessage(0); break;

default:

return DefWindowProc(hWnd, uMsg, wParam, lParam);

}

return 0;

}

//==================================================================== DWORD WINAPI ThreadFunc(LPVOID lpv)

{

IncCounter(); return 0;

}

//==================================================================== void IncCounter()

{

for (int i = 0; i < N; ++i) ++g_counter;

}

//==================================================================== void DecCounter()

{

for (int i = 0; i < N; ++i) --g_counter;

}

//////////////////////////////////////////////////////////////////////

458

Глава 9. Многозадачность

 

 

Первичный поток создает с помощью CreateThread дочерний поток, имеющий входную функцию ThreadFunc. В теле функции ThreadFunc вызывается функция IncCounter, которая выполняет в цикле N раз операцию инкремента для глобаль" ного счетчика g_counter.

Запустив дочерний поток и не дожидаясь окончания его выполнения, первич" ный поток вызывает функцию DecCounter. Функция DecCounter выполняет в цикле N раз операцию декремента для глобального счетчика g_counter.

После возврата из DecCounter первичный поток ждет окончания работы дочер" него потока с помощью функции WaitForSingleObject. Вызов функции

WaitForSingleObject(hThread, INFINITE);

обеспечивает приостановку выполнения потока на неограниченное время (INFINITE), пока не освободится объект ядра hThread.

После этого, вызывая функцию InvalidateRect, первичный поток заставляет Windows сформировать сообщение WM_PAINT. Обрабатывая это сообщение, функ" ция WndProc выводит значение переменной g_counter в главное окно приложения. Нетрудно догадаться, что правильным результатом выполнения этого приложе" ния должно быть значение g_counter = 0.

При маленьких значениях N так и происходит. Но вот для значения 50 000 000 эта программа дает самые разные результаты на разных компьютерах. Экспери" менты проводились в основном на компьютерах, имеющих процессор Intel Celeron и Intel Pentium. Заметим, что при тактовой частоте 2 ГГц функция IncCounter так же, как и функция DecCounter, требует для своего выполнения примерно 150 мс. При такой продолжительности выполнения каждый поток будет прерываться не менее пяти раз, отдавая процессор другому потоку.

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

Чем же вызвано искажение результата работы программы? — Прерыванием потока в тот момент, когда процессор не полностью завершил очередную опера" цию! Рассмотрим это чуть подробнее.

Допустим, что функция DecCounter первичного потока приступила к очередно" му вычитанию единицы из счетчика. Процессор, реализуя эту операцию, скопи" ровал значение глобальной переменной g_counter в свой регистр. Предположим, что это значение равно 100 000. Далее процессор успел вычесть единицу, но не успел переписать новое значение 99 999 обратно в глобальную переменную. В этот момент система отбирает процессор у первичного потока, сохранив контекст по" тока, и передает его на использование дочернему потоку. Предположим, что функ" ция IncCounter дочернего потока успела 50 000 раз добавить в счетчик единицу, так что переменная g_counter стала равна 150 000. В этот момент квант дочернего потока истек, и после окончания очередной операции система возвращает процес" сор первичному потоку. При этом система восстанавливает контекст первичного потока с незавершенной операцией. Завершая эту операцию, процессор переписы" вает значение 99 999 из своего регистра в глобальную переменную g_counter. В описанной ситуации квант работы дочернего потока пошел насмарку — его ре" зультаты утеряны!

Итак, мы показали, что одновременное использование несколькими потоками общей глобальной переменной без должной синхронизации может быть источни" ком ошибок.

Синхронизация

459

 

 

Синхронизация

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

совместно используя разделяемый ресурс (чтобы не разрушить его);

когда нужно уведомить другие потоки о завершении каких"либо операций.

Примитив синхронизации — это объект, который помогает управлять многопо" точным приложением. Основными типами примитивов синхронизации в Win" dows 2000 являются:

атомарные операции API"уровня;

критические секции;

события;

ожидаемые таймеры;

семафоры;

мьютексы.

Каждый из указанных типов применяется в определенных ситуациях.

Атомарный доступ

и семейство Interlocked-функций

Большая часть синхронизации потоков связана с атомарным доступом (atomic access) — монопольным захватом ресурса обращающимся к нему потоком. Win32 API предоставляет несколько функций для реализации взаимно блокированных операций. Все Interlocked"функции работают корректно только при условии, что их аргументы выровнены по границе двойного слова (DWORD).

Функция InterlockedIncrement, имеющая прототип

LONG InterlockedIncrement(LPLONG lpAddend);

инкрементирует 32"разрядную переменную, адрес которой задается параметром lpAddend. Функция возвращает новое значение указанной переменной.

Функция InterlockedDecrement определена аналогично функции InterlockedIncrement, но она декрементирует 32"разрядную переменную.

Пара функций

LONG InterlockedExchange(LPLONG lpTarget, LONG Value);

PVOID InterlockedExchangePointer(PVOID* ppvTarget, PVOID pvValue);

монопольно заменяет текущее значение переменной типа LONG, адрес которой пе" редается в первом параметре, значением, передаваемым во втором параметре. В 32"разрядном приложении обе функции работают с 32"разрядными значения" ми. В 64"разрядной программе первая функция оперирует 32"разрядными значе" ниями, а вторая — 64"разрядными. Обе функции возвращают исходное значение переменной.

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

LONG InterlockedExchangeAdd(LPLONG lpAddend, LONG Increment);

460 Глава 9. Многозадачность

Еще две функции выполняют операцию сравнения и присваивания по резуль" тату сравнения:

LONG InterlockedCompareExchange(LPLONG lpDestination, LONG Exchange,

LONG Comparand);

PVOID InterlockedCompareExchangePointer(PVOID* ppvDestination,

PVOID pvExchange, PVOID pvComparand);

Если значение переменной, адрес которой передается в первом параметре, со" впадает со значением, передаваемым в третьем параметре, то оно заменяется зна" чением, передаваемым во втором параметре. В 32"разрядном приложении обе функции работают с 32"разрядными значениями. В 64"разрядной программе пер" вая функция оперирует 32"разрядными значениями, а вторая — 64"разрядными. Обе функции возвращают исходное значение переменной, заданной первым па" раметром.

Вернемся к нашему приложению BadCount (см. листинг 9.3), работающему некорректно из"за одновременного доступа к общей глобальной переменной g_counter из разных потоков. Имея на вооружении функции с атомарным досту" пом, совсем несложно заставить приложение работать правильно. Для этого нуж" но в реализации функции IncCounter заменить инструкцию

++g_counter;

вызовом следующей функции:

InterlockedIncrement(&g_counter);

Аналогично, в реализации функции DecCounter необходимо операцию

--g_counter;

заменить следующим вызовом:

InterlockedDecrement(&g_counter);

Проверьте, как будет работать на вашем компьютере программа BadCount пос" ле указанных изменений в ее исходном тексте. Модифицированное приложение, по"видимому, достойно и нового имени GoodCount!

Критические секции

Критическая секция (critical section) — это небольшой участок кода, который дол" жен использоваться только одним потоком одновременно. Если в одно время не" сколько потоков попытаются получить доступ к критическому участку, то конт" роль над ним будет предоставлен только одному из потоков, а все остальные будут переведены в состояние ожидания до тех пор, пока участок не освободится.

Для использования критической секции необходимо определить переменную типа CRITICAL_SECTION:

CRITICAL_SECTION cs;

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

InitializeCriticalSection(&cs);

Чтобы завладеть критическим участком, поток должен вызвать функцию

EnterCriticalSection:

EnterCriticalSection(&cs);