лекции / Shchupak_Yu._Win32_API_Razrabotka_prilozheniy_dlya_Windows
.pdfУправление потоками |
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. В описанной ситуации квант работы дочернего потока пошел насмарку — его ре" зультаты утеряны!
Итак, мы показали, что одновременное использование несколькими потоками общей глобальной переменной без должной синхронизации может быть источни" ком ошибок.
