Пример простейшей многопоточной программы на WinAPI

25 декабря 2013

Продолжаем вспоминать Windows API. До сих пор мы писали грустные и унылые однопоточные программки из серии «вызови правильную процедуру с нужными аргументами и посмотри на ее код возврата». Сегодня же мы наконец-то напишем хоть и простенькую, но все же самую что ни на есть многопоточную программу, с теми самыми тредами и мьютексами, которые все постоянно ругают.

Из новых процедур нам понадобятся следующие.

CreateThread(NULL, 0, &ThreadProc, &hMutex, 0, NULL);

Как нетрудно догадаться, CreateThread создает новую нитку. Аргументы слева направо — (1) нечто, о чем сейчас знать не нужно, (2) размер стека в байтах, округляется до размера страницы, если ноль, то берется размер по умолчанию, (3) указатель на процедуру, с которой следует начать выполнение потока, (4) аргумент процедуры, переданной предыдущим аргументом, обычно здесь передается указатель на некую структуру, (5) флаги, например, можно создать приостановленный поток (CREATE_SUSPENDED), а затем запустить его с помощью ResumeThread, (6) куда записать ThreadId созданного потока. В случае успеха процедура возвращает хэндл созданного потока. В случае ошибки возвращается NULL, а подробности можно узнать через GetLastError.

CreateMutex(NULL, FALSE, NULL);

Процедура CreateMutex создает новый мьютекс. О первом аргументе сейчас знать не нужно. Если второй аргумент равен TRUE, созданный мьютекс будет сразу залочен текущим потоком, если же второй аргумент равен FALSE, создается разлоченный мьютекс. Третий аргумент задает имя мьютекса, если мы хотим создать именованный мьютекс. Именованные мьютексы нам пока что не понадобятся, поэтому передаем NULL. Возвращаемые значения в точности такие же, как и у CreateThread.

WaitForSingleObject(hHandle, dwTimeout);

WaitForSingleObject ждет, когда объект, хэндл которого был передан первым аргументом, перейдет в сигнальное состояние (signaled state). Кроме того, в зависимости от типа объекта, процедура может менять его состояние. WaitForSingleObject может быть применен к мьютексам, семафорам, потокам, процессам и не только. Если hHandle представляет собой хэндл мьютекса, процедура ждет, когда мьютекс освободится, а затем лочит его. Если же hHandle является хэндлом потока, то процедура просто ждет его завершения. Второй аргумент задает время ожидания в миллисекундах. Можно ждать вечно, передав специальное значение INFINITE. Если указать ноль, процедура не переходит в режим ожидания, а возвращает управление немедленно.

В случае ошибки процедура возвращает WAIT_FAILED, а подробности поможет узнать GetLastError. В случае успеха возвращается WAIT_OBJECT_0, если мы дождались перехода объекта в сигнальное состояние, и WAIT_TIMEOUT, если отвалились по таймауту. Также мы можем получить WAIT_ABANDONED. Это происходит в случае, если нить, державшая мьютекс, завершилась, не освободив его. В этом случае мьютекс становится залочен текущей нитью, но целостность данных, доступ к которым ограничивался мьютексом, по понятным причинам находится под вопросом.

WaitForMultipleObjects(dwCount, lpHandles, bWaitAll, dwTimeout);

Процедура WaitForMultipleObjects работает аналогично WaitForSingleObject, но для массива объектов. Первый аргумент задает размер массива, должен быть строго больше нуля и не превышать MAXIMUM_WAIT_OBJECTS (который равняется в точности 64). Вторым аргументом задается указатель на массив хэндлов. В массиве могут содержаться хэндлы на объекты разных типов, но многократное включение одного и того же хэндла считается ошибкой. Если третий аргумент равен TRUE, процедура ждет перехода в сигнальное состояние всех объектов, иначе — любого из указанных объектов. Семантика последнего аргумента точно такая же, как и в случае с WaitForSingleObject.

Процедура возвращает WAIT_FAILED в случае ошибки и WAIT_TIMEOUT в случае отваливания по таймауту. Если процедура вернула значение от WAIT_OBJECT_0 до WAIT_OBJECT_0 + dwCount - 1, то в случае bWaitAll == TRUE все объекты из массива перешли в сигнальное состояние, а в случае bWaitAll == FALSE в сигнальное состояние перешел объект с индексом код возврата минус WAIT_OBJECT_0. Если процедура вернула значение от WAIT_ABANDONED_0 до WAIT_ABANDONED_0 + dwCount - 1, то в случае bWaitAll == TRUE все ожидаемые объекты перешли в сигнальное состояние и по крайней мере один из объектов оказался брошенным мьютексом, а в случае bWaitAll == FALSE брошенным оказался мьютекс с индексом код возврата минус WAIT_ABANDONED_0.

Если bWaitAll == TRUE, процедура не меняет состояния объектов до тех пор, пока все они не перейдут в сигнальное состояние. Таким образом, пока вы ждете, мьютексы могут продолжать лочиться и анлочиться другими потоками. Если bWaitAll == FALSE и сразу несколько объектов перешли в сигнальное состояние, процедура всегда работает с объектом в массиве, имеющим минимальный индекс.

ReleaseMutex(hMutex);

ReleaseMutex анлочит ранее залоченный мьютекс. В случае успеха процедура возвращает значение, отличное от нуля. В случае ошибки возвращается ноль, а подробности доступны через GetLastError. Что характерно, для предотвращения дэдлоков Windows позволяет потокам лочить один и тот же мьютекс несколько раз, но и количество вызовов ReleaseMutex должно быть соответствующим.

Sleep(dwMilliseconds);

Процедура Sleep блокирует текущий поток на заданное количество миллисекунд. Если в качестве параметра передать INFINITE, поток будет заблокирован навсегда. Если передать ноль, поток уступает оставшуюся часть своей доли процессорного времени любой другой нити с таким же приоритетом. У процедуры нет возвращаемого значения (VOID).

ExitThread(dwCode);

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

А теперь посмотрим на все это хозяйство в действии:

#include <windows.h>

#define THREADS_NUMBER 10
#define ITERATIONS_NUMBER 100
#define PAUSE 10 /* ms */

DWORD dwCounter = 0;

DWORD WINAPI ThreadProc(CONST LPVOID lpParam) {
  CONST HANDLE hMutex = (CONST HANDLE)lpParam;
  DWORD i;
  for(i = 0; i < ITERATIONS_NUMBER; i++) {
    WaitForSingleObject(hMutex, INFINITE);
    dwCounter++;
    ReleaseMutex(hMutex);
    Sleep(PAUSE);
  }
  ExitThread(0);
}

VOID Error(CONST HANDLE hStdOut, CONST LPCWSTR szMessage) {
  DWORD dwTemp;
  TCHAR szError[256];
  WriteConsole(hStdOut, szMessage, lstrlen(szMessage), &dwTemp, NULL);
  wsprintf(szError, TEXT("LastError = %d\r\n"), GetLastError());
  WriteConsole(hStdOut, szError, lstrlen(szError), &dwTemp, NULL);
  ExitProcess(0);
}

INT main() {
  TCHAR szMessage[256];
  DWORD dwTemp, i;
  HANDLE hThreads[THREADS_NUMBER];
  CONST HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
  CONST HANDLE hMutex = CreateMutex(NULL, FALSE, NULL);
  if(NULL == hMutex) {
    Error(hStdOut, TEXT("Failed to create mutex.\r\n"));
  }

  for(i = 0; i < THREADS_NUMBER; i++) {
    hThreads[i] = CreateThread(NULL, 0, &ThreadProc, hMutex, 0, NULL);
    if(NULL == hThreads[i]) {
      Error(hStdOut, TEXT("Failed to create thread.\r\n"));
    }
  }

  WaitForMultipleObjects(THREADS_NUMBER, hThreads, TRUE, INFINITE);
  wsprintf(szMessage, TEXT("Counter = %d\r\n"), dwCounter);
  WriteConsole(hStdOut, szMessage, lstrlen(szMessage), &dwTemp, NULL);

  for(i = 0; i < THREADS_NUMBER; i++) {
    CloseHandle(hThreads[i]);
  }
  CloseHandle(hMutex);
  ExitProcess(0);
}

Здесь мы создаем десять потоков, каждый из которых увеличивает глобальный счетчик на единицу сто раз. Чтобы в счетчике не оказалась записана фигня, доступ к нему ограничивается с помощью мьютекса. Перед вызовом ExitProcess закрываются хэндлы мьютекса и всех созданных потоков. Строго говоря, в данном конкретном случае это не требуется, но в более общем случае все хэндлы за собой, разумеется, нужно вовремя закрывать. Также вы могли обратить внимание, что я прислушался к совету, который мне дали в комментариях к предыдущей заметке, и стал оборачивать строки в макрос TEXT.

Как обычно, программа может быть скомпилирована как в Visual Studio и запущена под Windows, так и с помощью MinGW и запущена под Wine. Ну и по традиции напоминаю, что ничто не делает блогера более грустным котиком, чем отсутствие каких-либо комментариев к его постам :)

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

Дополнение: Также вас могут заинтересовать статьи Многопоточность в C/C++ с использованием pthreads и Написание многопоточных приложений на C++11 и старше.

Метки: , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.