多线程 c++11-ish 队列在 Windows 上失败

Posted

技术标签:

【中文标题】多线程 c++11-ish 队列在 Windows 上失败【英文标题】:Multithreaded c++11-ish queue fails on windows 【发布时间】:2014-07-24 07:36:28 【问题描述】:

我不太喜欢多线程,所以我很感激任何建议。 在我以生产者-消费者多线程样式编写的服务器中queue 与其mutexcv 一起包装:

template <typename Event>
struct EventsHandle 
public: // methods:
    Event*
    getEvent()
    
            std::unique_lock<std::mutex> lock mutex;
            while (events.empty()) 
                    condition_variable.wait(lock);
            
            return events.front();
    ;

    void
    setEvent(Event* event)
    
            std::lock_guard<std::mutex> lock mutex;
            events.push(event);
            condition_variable.notify_one();
    ;

    void
    pop()
     events.pop(); ;

private: // fields:
    std::queue<Event*> events;
    std::mutex mutex;
    std::condition_variable condition_variable;
;

以及它在消费者线程中的使用方式:

void
Server::listenEvents()

    while (true) 
            processEvent(events_handle.getEvent());
            events_handle.pop();
    
;

在生产者中:

    parse input, whatever else
    ...
    setEvent(new Event ERASE_CLIENT, getSocket(), nullptr);
    ...

void
Client::setEvent(Event* event)

    if (event) 
            events_handle->setEvent(event);
    
;

代码在 linux 上工作,我不知道为什么,但在 windows MSVC13 上失败。 此对话框有时会引发异常:"Unhandled exception at 0x59432564 (msvcp120d.dll) in Server.exe: 0xC0000005: Access violation reading location 0xCDCDCDE1". 调试显示在此行上引发了异常:setEvent() 函数中的std::lock_guard&lt;std::mutex&gt; lock(mutex)

通过谷歌搜索,我找到了这些文章:http://www.justsoftwaresolutions.co.uk/threading/implementing-a-thread-safe-queue-using-condition-variables.htmlhttp://www.codeproject.com/Articles/598695/Cplusplus-threads-locks-and-condition-variables

我试图跟随他们,但没有得到什么帮助。所以在这个长文本表之后我的问题是: 代码有什么问题,互斥锁?

更新 所以...最后,在名为“comment that line out”的可爱游戏之后,结果发现问题出在内存管理上。事件的 dctor 导致失败。 作为结论,在来回传递对象时,最好使用 std::unique_ptr 作为提及 Jarod42 或使用值语义作为建议 juanchopanza。 并尽可能使用库,不要重新发明*** =)

【问题讨论】:

你有一个消费者吗?否则,您应该分别对getEvent()pop() 进行同步访问,即将它们放在队列中的一个单一同步方法中。另外(对于自我广告感到抱歉),我实现了一个概念验证,极其简单和愚蠢的并发队列here。底部有一个指向源代码的 github 链接。看看它是否适合你会很有趣。 @juanchopanza 我刚刚尝试将 pop() 移动到 getEvent() ,例如:'Event* tmp = events.front();事件.pop();返回 tmp;没有改变。不,我有很多制作人。 注意:“消费者”之所以被称为“消费者”,是因为它会消费一些东西。那东西不能留给其他人以后尝试消费。 我会使用std::unique_ptr&lt;Event&gt; 而不是原始的Event*,这样可以避免内存管理和悬空指针。 @Aenry 好吧,当生产者添加事件并使队列重新分配时,指向消费者持有的事件的指针可能会失效。你不应该真的返回指针。使用值语义。 【参考方案1】:

由于EventsHandle::pop 中缺少互斥体,您存在数据竞争。

您的生产者线程可以通过调用setEvent() 将项目推入队列,并在执行events.push(event) 行时被抢占。现在消费者线程可以同时执行events.pop()。您最终会在 queue 上进行两次不同步的写入,这是未定义的行为。

另请注意,如果您有多个消费者,则需要确保您弹出的元素与您之前从getEvent 检索到的元素相同。如果一个消费者在两个调用之间被另一个消费者抢占。这很难通过两个独立的成员函数来实现,这两个成员函数由作为该类成员的互斥锁同步。这里通常的方法是提供一个getEventAndPop() 函数,它在整个操作过程中保持锁定并摆脱您当前拥有的单独函数。乍一看,这似乎是一个荒谬的限制,但多线程代码必须遵循不同的规则。

【讨论】:

即使使用同步的pop(),由于队列返回指向数据的指针,而不是值,一个线程调用pop() 可能会给另一个线程留下悬空指针。 @juanchopanza 仅当一个线程被允许弹出另一个检索到的元素时,我的最后一段希望解决这个问题。 但是你怎么能保证呢?这是一个队列。你消费和弹出任何发生在前面的东西。 @juanchopanza 或者通过外部同步(这很丑陋并且在某种程度上破坏了线程安全队列的概念),或者通过更改接口以提供retrieveAndPop 没错。这就是我试图提出的观点。两个调用在并发队列的接口中不能分开。【参考方案2】:

您可能还想将setEvent 方法更改为在互斥锁仍处于锁定状态时不通知。这取决于调度程序,但等待通知的线程可能会立即唤醒以等待互斥锁。

void 
setEvent(Event* event) 

    
        std::lock_guard<std::mutex> lockmutex;
        events.push(event);
    
    condition_variable.notify_one();
;

【讨论】:

除非你必须关心通知消费者的优先级反转。见this page for details。另请注意,许多现代实现实际上足够智能,如果相应的锁尚未释放,则不会唤醒在条件变量上阻塞的线程。 有趣。谢谢你的链接!【参考方案3】:

在我看来,pop 应该集成在 getEvent() 中。

这将防止更多线程获得相同的事件(如果 pop 不在 getEvent 中,则更多线程可能获得相同的事件)。

Event*
getEvent()

    std::unique_lock<std::mutex> lock mutex;
    while (events.empty()) 
            condition_variable.wait(lock);
    
    Event* front = events.front();
    events.pop();
    return front;
;

【讨论】:

以上是关于多线程 c++11-ish 队列在 Windows 上失败的主要内容,如果未能解决你的问题,请参考以下文章

C/C++线程安全型队列的实现

iOS 开发--多线程

c++ windows中客户端服务器编程中的多线程

高手进,关于C语言在windows上建立多线程的问题(VC6.0上实现)

多线程批处理队列

C++ 无锁队列与多线程崩溃