2017-02-21 74 views
3

我想写一个包装std :: thread的类,它的行为像std :: thread,但每次我需要处理某些异步时都没有实际分配一个线程。原因是我需要在不允许动态分配的上下文中使用多线程,而且我也不想要创建std :: thread的开销。编写一个活着的线程

取而代之,我想要一个线程在循环中运行并等待它可以开始处理。客户呼叫invoke唤醒线程。线程锁定一个互斥锁,它是否正在处理并再次入睡。函数join的行为类似于std :: thread :: join,直到线程释放锁(即再次入睡)为止。

我想我得到了课程,但由于多线程的普遍缺乏经验,我想问问有没有人可以发现比赛条件,或者如果我使用的方法被认为是“好风格”。例如,我不确定是否临时锁定互斥体是一种体面的方式来“加入”线程。

编辑 我发现了另一个竞争条件:调用join后直接invoke时,没有理由线程已经锁定互斥体,因此直到线程进入睡眠状态锁定的join调用者。为了防止这种情况,我必须为invoke计数器添加一个检查。

#pragma once 

#include <thread> 
#include <atomic> 
#include <mutex> 

class PersistentThread 
{ 
public: 
    PersistentThread(); 
    ~PersistentThread(); 

    // set function to invoke 
    // locks if thread is currently processing _func 
    void set(const std::function<void()> &f); 

    // wakes the thread up to process _func and fall asleep again 
    // locks if thread is currently processing _func 
    void invoke(); 

    // mimics std::thread::join 
    // locks until the thread is finished with it's loop 
    void join(); 

private: 

    // intern thread loop 
    void loop(bool *initialized); 

private: 

    bool       _shutdownRequested{ false }; 

    std::mutex      _mutex; 

    std::unique_ptr<std::thread> _thread; 
    std::condition_variable   _cond; 

    std::function<void()>   _func{ nullptr }; 
}; 

源文件

#include "PersistentThread.h" 

    PersistentThread::PersistentThread() 
{ 
    auto lock = std::unique_lock<std::mutex>(_mutex); 
    bool initialized = false; 

    _thread = std::make_unique<std::thread>(&PersistentThread::loop, this, &initialized); 

    // wait until _thread notifies, check bool initialized to prevent spurious wakeups 
    _cond.wait(lock, [&] {return initialized; }); 
} 

PersistentThread::~PersistentThread() 
{ 
    { 
     std::lock_guard<std::mutex> lock(_mutex); 

     _func = nullptr; 
     _shutdownRequested = true; 

     // wake up and let join 
     _cond.notify_one(); 
    } 

    // join thread, 
    if (_thread->joinable()) 
    { 
     _thread->join(); 
    } 
} 

void PersistentThread::set(const std::function<void()>& f) 
{ 
    std::lock_guard<std::mutex> lock(_mutex); 
    this->_func = f; 
} 

void PersistentThread::invoke() 
{ 
    std::lock_guard<std::mutex> lock(_mutex); 
    _cond.notify_one(); 
} 

void PersistentThread::join() 
{ 
    bool joined = false; 
    while (!joined) 
    { 
     std::lock_guard<std::mutex> lock(_mutex); 
     joined = (_invokeCounter == 0); 
    } 
} 

    void PersistentThread::loop(bool *initialized) 
{ 

    std::unique_lock<std::mutex> lock(_mutex); 
    *initialized = true; 
    _cond.notify_one(); 

    while (true) 
    {  
     // wait until we get the mutex again 
     _cond.wait(lock, [this] {return _shutdownRequested || (this->_invokeCounter > 0); }); 

     // shut down if requested 
     if (_shutdownRequested) return; 

     // process 
     if (_func) _func(); 
     _invokeCounter--; 

    } 
} 
+5

听起来像你想要[线程池](https://en.wikipedia.org/wiki/Thread_pool) – NathanOliver

+0

也许这应该被张贴在Codereview SE? –

+0

@NathanOliver我第二。只需在开始时立即创建一些线程,然后使用它们。 – Carcigenicate

回答

1

您正在询问潜在的竞争条件,并且我在所示的代码中看到至少一个竞争条件。

构建PersistentThread后,不能保证新线程将在主执行线程从构造函数返回并进入invoke()之前获取其loop()中的初始锁。主执行线程有可能在构造函数完成后立即输入invoke(),最终通知nobody,因为内部执行线程尚未锁定互斥量。因此,这个invoke()将不会导致任何处理发生。

您需要将构造函数的完成与执行线程的初始锁获取同步。

编辑:你的修改看起来正确;但我也发现了另一种竞赛状况。

由于documented in the description of wait()wait()可能会“虚假地”醒来。仅仅因为wait()返回,并不意味着其他某个线程已输入invoke()

除了其他所有内容外,您还需要一个计数器,invoke()递增计数器,执行线程仅在计数器大于零时执行其分配的职责,然后递减计数器。这将防止虚假的唤醒。

我本来也有执行线程进入wait()前检查柜台,并进入wait()仅当它是0。否则,它递减计数器,执行其功能,并循环回。

这应该堵塞这个领域所有潜在的竞争条件。

P.S.虚假唤醒还适用于初始通知,在您的更正中,执行线程已进入循环。你也需要为这种情况做类似的事情。

+0

解决方法是在创建线程之前添加'auto lock = std :: unique_lock (_mutex);'在创建之后添加'_cond.wait(lock);'然后让线程调用'_cond.notify_one ();'在创建本地锁后?我也可以编辑问题中的源代码,... –

+0

添加了我认为是源代码中的解决方案 –

+0

是的,但是您的工作尚未完成,编辑... –

0

我不明白你要问什么。这是你使用的一种很好的风格。

使用bools会更安全并检查单个routines,因为void不会返回任何内容,因此您可能会由于错误而导致卡住。自线程在引擎盖下运行后,检查所有可以使用的内容。如果过程真的成功,请确保呼叫正确运行。你也可以阅读一些关于“线程池”的东西。

+0

您是否在讨论使用bools作为'_func'的结果类型?我不太确定这将防止有问题的情况。我觉得要抓住例外情况,但这是关于它的。 –

+0

捕捉异常最终会是同样的感觉,只是更昂贵 – Lazcano

+0

@Lazcano异常是当它们不存在时是零成本的。但是,当他们这样做时,他们的成本要高得多,但这不应该很重要,因为这在大多数情况下并不经常发生。 – ProXicT

相关问题