Как избегаеть data races используя semaphore?
Начал работать из semaphore (WinAPI) и не могу понять как при его использовании избегать data races. Например:
#include <windows.h>
#include <iostream>
HANDLE semaphore;
const int THREADS_COUNT = 10;
const int SEMAPHORE_MAX_VALUE = 3;
int global = 0;
DWORD WINAPI func(LPVOID)
{
DWORD waitResult = WaitForSingleObject(semaphore, 0);
switch (waitResult)
{
case WAIT_OBJECT_0:
for (int i = 0; i < 100000; i++)
{
global++;
}
ReleaseSemaphore(semaphore, 1, NULL);
break;
case WAIT_TIMEOUT:
std::cout << "ID = " << GetCurrentThreadId() << " wait\n";
break;
}
Sleep(3000);
return 0;
}
int main()
{
semaphore = CreateSemaphore(NULL, SEMAPHORE_MAX_VALUE, SEMAPHORE_MAX_VALUE, NULL);
HANDLE threads[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; ++i)
{
threads[i] = CreateThread(NULL, 0, func, NULL, 0, NULL);
}
WaitForMultipleObjects(THREADS_COUNT, threads, true, INFINITE);
for (auto& el : threads)
{
CloseHandle(el);
}
CloseHandle(semaphore);
std::cout << "global = " << global;
return 0;
}
в итоге значение global всегда разное. Как єтого избежать? Мне нужно использовать например еще CRITICAL_SECTION? Или я вообще неправильно использую semaphore? Буду благодарен за ответ и возможно даже пример кода по правильному использованию semaphor'a =)
Ответы (2 шт):
Основная проблема вашего кода в том, что семафор изначально создается со со значением счетчика до SEMAPHORE_MAX_VALUE, тем самым позволяя до SEMAPHORE_MAX_VALUE потоков одновременно дождаться WaitForSingleObject и начать работу. Тогда как в данном случае для предотвращения состояния гонки возможность работать и модифицировать global должен иметь максимум один поток. По сути в этой ситуации нужен семафор со счетчиком до 1, то бишь работающий как аналог CRITICAL_SECTION. То бишь семафор тут не нужен, вместо него следует использовать CRITICAL_SECTION, а еще лучше - SRW lock (не являются объектами ядра, соответственно устроены проще и работают шустрее, но только в рамках одного процесса).
В данном конкретном примере с веселым счетчиком, примитивы синхронизации не нужны совсем. Для этого есть std::atomic<>
- никто не будет дергать ядро
- код более читабельный
- вы не привязаны к конкретной ОС
Вообще, начиная с C++11, первым приоритетом выбора для синхронизации являются средства самого языка, а где-то третьим-четвертым (после boost, Qt, libpthread наконец) будут таки примитивы из API ОС.