通缉:优雅的竞争条件解决方案

时间:2009-09-10 17:36:01

标签: c++ multithreading

我有以下代码:

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是将在非测试环境中使用的唯一方法。

3 个答案:

答案 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,操作系统线程池QueueUserWorkItemidempotent处理。将pushes条目添加到列表中并提交工作项。工作项pops是一个条目,如果不是NULL,则处理它。你可以花哨并为处理器提供额外的逻辑循环并保持一个标记其“活动”状态的状态,这样Add不会对不必要的工作项进行定位,但这并不是严格要求的。对于更高的sclae和多核/多CPU负载分布,我建议使用排队的完成端口。详细信息在Rick Vicik的文章中有所描述,我有一个博客文章,可以同时链接所有3个文章:High Performance Windows programs