Условные переменные, как и мьютексы, являются базовыми примитивами для синхронизации в параллельном программировании. К сожалению, классическая условная переменная в нотации потоков POSIX (pthread) сложно реализуема в Windows (судя по MSDN Windows таки поддерживают механизм условных переменных на уровне API, но не в XP или 2003, в чем-то более новом, увы). Мне потребовался для одного проекта простейший механизм синхронизации двух потоков: один поток ждет, не занимая ресурсов процессора, и активизируется, только когда другой поток его попросит об этом. Простейший триггер. Конечно, по логике — это обыкновенная условная переменная в упрощенном варианте. Для UNIX это реализуется именно через условную переменную потоков POSIX, а для Windows — через события.
Файл trigger.h
:
#ifndef _EXT_TRIGGER_H
#define _EXT_TRIGGER_H
#ifdef WIN32
#include <windows.h>
#else
#include <pthread.h>
#endif
namespace ext {
class Trigger {
public:
Trigger();
~Trigger();
// Функция посылки сигнала потоку,
// ждущему на функции Wait().
void Signal();
// Функция ожидания сигнала.
// Вызов этой функции приводит к блокировке потока до
// получения сигнала от функции Signal().
// Внимание: функция Signal() не должна быть вызвана до
// того, как ждущий поток "сядет" на Wait(). Подобное
// использование ведет к неопределенному поведению.
void Wait();
private:
#ifdef WIN32
HANDLE __handle;
#else
pthread_mutex_t __mutex;
pthread_cond_t __cv;
#endif
// "Защита" от случайного копирования.
Trigger(const Trigger&);
void operator=(const Trigger&);
};
} // namespace ext
#endif
Файл trigger.cpp
: #include "Trigger.h"
namespace ext {
#ifdef WIN32
Trigger::Trigger() {
__handle = CreateEvent(
NULL, // Атрибуты безопасности по умолчанию.
TRUE, // Режим ручной активации события.
FALSE, // Начальное состояния -- неактивное.
NULL // Безымянное событие.
);
}
Trigger::~Trigger() {
CloseHandle(__handle);
}
void Trigger::Signal() {
SetEvent(__handle);
}
void Trigger::Wait() {
// Ждем наступление события.
WaitForSingleObject(__handle, INFINITE);
// "Перезаряжаем" событие.
ResetEvent(__handle);
}
#else // WIN32
Trigger::Trigger() {
pthread_mutex_init(&__mutex, NULL);
pthread_cond_init(&__cv, NULL);
}
Trigger::~Trigger() {
pthread_cond_destroy(&__cv);
pthread_mutex_destroy(&__mutex);
}
void Trigger::Signal() {
pthread_mutex_lock(&__mutex);
pthread_cond_signal(&__cv);
pthread_mutex_unlock(&__mutex);
}
void Trigger::Wait() {
pthread_mutex_lock(&__mutex);
pthread_cond_wait(&__cv, &__mutex);
pthread_mutex_unlock(&__mutex);
}
#endif // WIN32
} // namespace ext
Пространство имен, как обычно, ext
, так что меняете по вкусу.
Проверим, как будет работать (естественно, через тест). Для тестирования также потребуются: класс Thread, класс PreciseTimer и Google Test. О том, как собрать себе компактную версию Google Test в виде всего двух файловФайлgtest-all.cc
иgtest.h
уже писал.
trigger_unittest.cpp
: #include <gtest/gtest.h>
#include "trigger.h"
#include "thread.h"
#include "pretimer.h"
// Тестовый поток, который будет "скакать" по указанным ключевым
// точкам, увеличивая значение счетчика.
class TriggerThread: public ext::Thread {
public:
TriggerThread(volatile int& flag, ext::Trigger& trigger) :
__flag(flag), __trigger(trigger)
{}
virtual void Execute() {
// Ждем первого сигнала.
__trigger.Wait();
__flag = 1;
// Ждем второго сигнала.
__trigger.Wait();
__flag = 2;
// Ждем третьего сигнала.
__trigger.Wait();
__flag = 3;
}
private:
volatile int& __flag;
ext::Trigger& __trigger;
};
TEST(Trigger, Generic) {
volatile int flag = 0;
ext::Trigger trigger;
// Создаем поток и запускаем егою
TriggerThread a(flag, trigger);
a.Start();
// Подождем, чтобы поток "сел" на Wait().
ext::PreciseTimer::sleepMs(10);
// Флаг не должен стать 1, так как поток
// должен ждать на Wait().
EXPECT_EQ(0, (int)flag);
// Информируем поток о событии.
trigger.Signal();
// Подождем, чтобы поток успел изменить флаг на 1.
ext::PreciseTimer::sleepMs(10);
// Проверим, как он это сделал.
EXPECT_EQ(1, (int)flag);
// Далее проверка повторяется еще пару раз, чтобы проверить,
// что синхронизирующий объект правильно "взводится" после
// срабатывания.
trigger.Signal();
ext::PreciseTimer::sleepMs(10);
EXPECT_EQ(2, (int)flag);
trigger.Signal();
a.Join();
// Последняя проверка не требует ожидания, так как мы присоединись
// к потоку, и он точно уже завершился.
EXPECT_EQ(3, (int)flag);
}
Компилируем для Windows в Visual Studio: cl /EHsc /I. /Fetrigger_unittest_vs2008.exe /DWIN32 runner.cpp ^
trigger.cpp trigger_unittest.cpp pretimer.cpp thread.cpp gtest\gtest-all.cc
или в GCC: g++ -I. -o trigger_unittest_vs2008.exe runner.cpp \
trigger.cpp trigger_unittest.cpp pretimer.cpp thread.cpp gtest/gtest-all.cc
Запускаем: [==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from Trigger
[ RUN ] Trigger.Generic
[ OK ] Trigger.Generic (31 ms)
[----------] 1 test from Trigger (47 ms total)
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (78 ms total)
[ PASSED ] 1 test.
Работает. Внимательный читатель заметит, что по хорошему бы надо протестировать случай, когда функцияSignal()
вызывается раньше, чем слушающий поток дойдет доWait()
. Как сказано в комментариях, эта ситуация считается логической ошибкой и ведет к неопределенному поведению. В жизни получается так: реализация для Windows считает, что если функцияSignal()
была вызвана доWait()
, тоWait()
просто тут же выходит, как бы получив сигнал сразу при старте. Реализация же под UNIX работает иначе:Wait()
отрабатывает только те вызовыSignal()
, которые были сделаны после начала самогоWait()'а
. Самое настоящее неопределенное поведение. При использовании данного класса надо помнить об этом ограничении.
Другие посты по теме:
По поводу неопределенного поведения: можно ввести флаг в реализацию для *nix, который будет устанавливаться в true, если сигнал получен раньше запуска wait, и при вызове wait не вызывать в таком случае pthread_cond_wait(). Тогда. судя по всему, получится такое же поведение, как и в версии для Win.
ОтветитьУдалить