我有以下代码:
class TimeOutException
{};
template <typename T>
class MultiThreadedBuffer
{
public:
MultiThreadedBuffer()
{
InitializeCriticalSection(&m_csBuffer);
m_evtDataAvail = CreateEvent(NULL, TRUE, FALSE, NULL);
}
~MultiThreadedBuffer()
{
CloseHandle(m_evtDataAvail);
DeleteCriticalSection(&m_csBuffer);
}
void LockBuffer()
{
EnterCriticalSection(&m_csBuffer);
}
void UnlockBuffer()
{
LeaveCriticalSection(&m_csBuffer);
}
void Add(T val)
{
LockBuffer();
m_buffer.push_back(val);
SetEvent(m_evtDataAvail);
UnlockBuffer();
}
T Get(DWORD timeout)
{
T val;
if (WaitForSingleObject(m_evtDataAvail, timeout) == WAIT_OBJECT_0) {
LockBuffer();
if (!m_buffer.empty()) {
val = m_buffer.front();
m_buffer.pop_front();
}
if (m_buffer.empty()) {
ResetEvent(m_evtDataAvail);
}
UnlockBuffer();
} else {
throw TimeOutException();
}
return val;
}
bool IsDataAvail()
{
return (WaitForSingleObject(m_evtDataAvail, 0) == WAIT_OBJECT_0);
}
std::list<T> m_buffer;
CRITICAL_SECTION m_csBuffer;
HANDLE m_evtDataAvail;
};
单元测试显示,只要T的默认构造函数和复制/赋值运算符不抛出,此代码在单个线程上使用时工作正常。由于我在写T,这是可以接受的。
我的问题是Get方法。如果没有可用的数据(即未设置m_evtDataAvail),则有几个线程可以阻止WaitForSingleObject调用。当新数据可用时,它们都会进入Lock()调用。只有一个会通过并可以获取数据并继续前进。在Unlock()之后,另一个线程可以继续进行,并且会发现没有数据。目前它将返回默认对象。
我想要发生的是第二个线程(以及其他线程)返回到WaitForSingleObject调用。我可以添加一个解锁并执行goto的其他块,但这只是感觉很邪恶。
该解决方案还增加了无限循环的可能性,因为每次返回都会重新启动超时。我可以添加一些代码来检查输入时的时钟并调整每次返回的超时,但是这个简单的Get方法开始变得非常复杂。
有关如何在保持可测试性和简单性的同时解决这些问题的任何想法?
哦,对于任何想知道的人来说,IsDataAvail函数仅用于测试。它不会在生产代码中使用。 Add和Get是将在非测试环境中使用的唯一方法。
答案 0 :(得分:7)
您需要创建自动重置事件而不是手动重置事件。这保证了如果多个线程正在等待事件,并且设置事件时只会释放一个线程。所有其他线程将保持等待状态。您可以通过将FALSE传递给CreateEvent API的第二个参数来创建自动重置事件。另请注意,此代码不是异常安全的,即在锁定缓冲区后,如果某个语句抛出异常,则您的临界区将不会被解锁。使用RAII原则确保您的关键部分即使在例外的情况下也能解锁。
答案 1 :(得分:5)
您可以使用Semaphore对象而不是通用Event对象。每次调用Add时,应使用ReleaseSemaphore将信号量计数初始化为0并递增1。这样,Get中的WaitForSingleObject将永远不会释放更多要从缓冲区读取的线程,而不是缓冲区中的值。
答案 2 :(得分:3)
您将始终必须为事件发出信号但不存在数据的情况进行编码,即使是自动重置事件也是如此。从WaitForsingleevent唤醒到调用LockBuffer的那一刻有一个竞争条件,并且在该间隔中,另一个线程可以从缓冲区弹出数据。您的代码必须将WaitForSingleEvent置于循环中。使用在每次循环迭代中花费的时间减少超时...
作为替代方案,我可能会对您更具可扩展性和高性能的替代方案感兴趣吗? Interlocked Singly Linked Lists,操作系统线程池QueueUserWorkItem和idempotent处理。将pushes条目添加到列表中并提交工作项。工作项pops是一个条目,如果不是NULL,则处理它。你可以花哨并为处理器提供额外的逻辑循环并保持一个标记其“活动”状态的状态,这样Add不会对不必要的工作项进行定位,但这并不是严格要求的。对于更高的sclae和多核/多CPU负载分布,我建议使用排队的完成端口。详细信息在Rick Vicik的文章中有所描述,我有一个博客文章,可以同时链接所有3个文章:High Performance Windows programs。