代码之家  ›  专栏  ›  技术社区  ›  Jere.Jones

需要:优雅的比赛条件解决方案

  •  3
  • Jere.Jones  · 技术社区  · 16 年前

    我有以下代码:

    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,那是可以接受的。

    我的问题是获取方法。如果没有可用的数据(即未设置m-evtdataavil),则可以在WaitForSingleObject调用上阻塞多个线程。当新数据可用时,它们都会进入lock()调用。只有一个会通过,并且可以获取数据并继续前进。在unlock()之后,另一个线程可以继续运行,将发现没有数据。当前它将返回默认对象。

    我要做的是让第二个线程(和其他线程)返回到waitForSingleObject调用。我可以添加一个解锁并执行goto的其他块,但这感觉很邪恶。

    该解决方案还增加了无限循环的可能性,因为每次回程都会重新启动超时。我可以添加一些代码来检查输入时的时钟,并在每次返回时调整超时,但是这个简单的get方法开始变得非常复杂。

    对于如何在保持可测试性和简单性的同时解决这些问题有什么想法吗?

    哦,对于任何想知道的人来说,isdataavail函数只存在于测试中。它不会在生产代码中使用。添加和获取是将在非测试环境中使用的唯一方法。

    3 回复  |  直到 16 年前
        1
  •  7
  •   Naveen    16 年前

    您需要创建自动重置事件而不是手动重置事件。这样可以保证,如果多个线程在等待一个事件,并且在设置该事件时,只释放一个线程。所有其他线程将保持等待状态。您可以通过向的第二个参数传递false来创建自动重置事件。 CreateEvent 应用程序编程接口。另外,请注意,此代码不是异常安全的,即在锁定缓冲区之后,如果某些语句引发异常,则关键部分将不会被解锁。使用 RAII 确保关键部分即使在异常情况下也能解锁的原则。

        2
  •  5
  •   Chris Dodd    16 年前

    您可以使用信号量对象而不是通用事件对象。每次调用add时,信号量计数都应初始化为0,并以releaseSemaphore递增1。这样,get中的waitForsingleObject将永远不会释放比缓冲区中的值更多的线程来读取缓冲区。

        3
  •  3
  •   Remus Rusanu    16 年前

    您必须始终为事件发出信号但没有数据的情况编写代码,即使使用自动重置事件也是如此。从waitForSingleEvent唤醒到调用lockBuffer,都存在一个争用条件,在此间隔内,另一个线程可以从缓冲区中弹出数据。代码必须将waitForSingleEvent放入循环中。随着在每个循环迭代中已经花费的时间减少超时…

    作为替代方案,我可以让您对更具可扩展性和性能的替代方案感兴趣吗? Interlocked Singly Linked Lists ,OS线程池 QueueUserWorkItem idempotent 处理。添加 pushes 进入列表并提交工作项的条目。工作项目 pops 一个条目,如果不为空,则处理它。您可以使用花哨的、额外的逻辑,让处理器循环并保持一个状态,标记其“活动”状态,这样添加就不会产生不必要的工作项,但这不是严格要求的。对于更高的SCLAE和多核/多CPU负载分布,我建议使用排队完成端口。这些细节在Rick Vicik的文章中有描述,我有一个博客条目可以同时链接所有3个站点: High Performance Windows programs .