在多线程编程中,经常需要一种机制来同步线程,以确保在访问共享资源时不会发生冲突。通常,会使用信号量(semaphore)对象来等待某个线程开始访问受保护的对象,并允许一定数量的线程同时访问。然而,有时候需要相反的功能:一种方法来知道所有线程何时完成了对某个对象的访问。本文将介绍一个简单的类,用于解决这个问题。
注意:作者更倾向于使用标准的C++11实现,但标准库中缺少了项目中需要的非常有用的WaitForMultipleObjects()函数。
反向信号量类通过创建互斥量(mutex)和事件(event)来实现线程同步。以下是该类的实现:
class reverse_semaphore {
private:
HANDLE hE = 0;
HANDLE hM = 0;
volatile unsigned long long m = 0;
reverse_semaphore(const reverse_semaphore&) = delete;
reverse_semaphore& operator=(const reverse_semaphore&) = delete;
public:
reverse_semaphore() {
m = 0;
hE = CreateEvent(0, TRUE, TRUE, 0);
hM = CreateMutex(0, 0, 0);
}
~reverse_semaphore() {
CloseHandle(hM);
hM = 0;
CloseHandle(hE);
hE = 0;
}
void lock() {
WaitForSingleObject(hM, INFINITE);
m++;
ResetEvent(hE);
ReleaseMutex(hM);
}
void unlock() {
WaitForSingleObject(hM, INFINITE);
if(m > 0) m--;
if(m == 0) SetEvent(hE);
ReleaseMutex(hM);
}
DWORD Wait(DWORD dw = INFINITE) {
return WaitForSingleObject(hE, dw);
}
void WaitAndLock() {
HANDLE h[2] = {hE, hM};
WaitForMultipleObjects(2, h, TRUE, INFINITE);
lock();
ReleaseMutex(hM);
}
HANDLE WaitAndBlock() {
HANDLE h[2] = {hE, hM};
WaitForMultipleObjects(2, h, TRUE, INFINITE);
return hM;
}
};
构造函数/析构函数:创建一个互斥量和一个事件用于工作。不允许复制该类。析构函数释放这些对象。
lock():原子地将使用计数器增加1。
unlock():原子地将使用计数器减少1。如果计数器达到0,则设置事件。
Wait():等待事件。当所有捕获对象的线程都释放了它时,事件被设置。
WaitAndLock():等待事件和互斥量,确保所有线程完成对对象的操作后,当前线程重新捕获它。这是最常用的函数。
WaitAndBlock():等待事件和互斥量,确保所有线程完成对对象的操作后,没有其他线程可以捕获对象。函数返回锁定的互斥量的句柄,调用线程在其独占访问对象完成后应使用ReleaseMutex()释放它。
使用WaitForMultipleObjects()是为了避免竞态条件。使用这个函数确保在所有线程释放对象之前,互斥量不会被拥有。如果没有它,函数可能在事件被设置后(即所有线程都完成时)继续执行,但在拥有互斥量之前,另一个线程可能捕获了对象。
以下是一个示例代码,创建10个线程锁定反向信号量,然后在计时器上解锁它。所有线程释放它后,打印一条消息。线程仍然运行5秒钟。
void TestRevSem() {
reverse_semaphore u;
vector threads;
for (int i = 0; i < 10; i++) {
threads.emplace_back([&](int slp) {
if (true) {
std::lock_guard lg(u);
Sleep((10 - slp) * 1000);
}
Sleep(5000);
}, i);
}
// 假设所有线程都已启动。为简单起见,避免额外检查。
u.Wait();
cout << "All threads released the object, threads still running";
for (auto& t : threads) t.join();
}