2013-07-24 142 views
4

我想使用C++ 11 std :: condition_variable,但是当我尝试锁定与第二个线程关联的unique_lock时,我得到一个异常“避免资源死锁”。创建它的线程可以锁定和解锁它,但不是第二个线程,尽管我非常确定unique_lock不应该在第二个线程试图锁定它的时候锁定。锁定C++ 11 std :: unique_lock导致死锁异常

FWIW我在Linux中使用gcc 4.8.1,使用-std = gnu ++ 11。

我已经在condition_variable,unique_lock和mutex周围编写了一个包装类,因此我的代码中没有其他东西可以直接访问它们。注意使用std :: defer_lock,我已经陷入了陷阱:-)。

class Cond { 
private: 
    std::condition_variable cCond; 
    std::mutex cMutex; 
    std::unique_lock<std::mutex> cULock; 
public: 
    Cond() : cULock(cMutex, std::defer_lock) 
    {} 

    void wait() 
    { 
     std::ostringstream id; 
     id << std::this_thread::get_id(); 
     H_LOG_D("Cond %p waiting in thread %s", this, id.str().c_str()); 
     cCond.wait(cULock); 
     H_LOG_D("Cond %p woke up in thread %s", this, id.str().c_str()); 
    } 

    // Returns false on timeout 
    bool waitTimeout(unsigned int ms) 
    { 
     std::ostringstream id; 
     id << std::this_thread::get_id(); 
     H_LOG_D("Cond %p waiting (timed) in thread %s", this, id.str().c_str()); 
     bool result = cCond.wait_for(cULock, std::chrono::milliseconds(ms)) 
       == std::cv_status::no_timeout; 
     H_LOG_D("Cond %p woke up in thread %s", this, id.str().c_str()); 
     return result; 
    } 

    void notify() 
    { 
     cCond.notify_one(); 
    } 

    void notifyAll() 
    { 
     cCond.notify_all(); 
    } 

    void lock() 
    { 
     std::ostringstream id; 
     id << std::this_thread::get_id(); 
     H_LOG_D("Locking Cond %p in thread %s", this, id.str().c_str()); 
     cULock.lock(); 
    } 

    void release() 
    { 
     std::ostringstream id; 
     id << std::this_thread::get_id(); 
     H_LOG_D("Releasing Cond %p in thread %s", this, id.str().c_str()); 
     cULock.unlock(); 
    } 
}; 

我的主线程创建了一个RenderContext,它有一个与它关联的线程。从主线程的角度来看,它使用Cond来指示渲染线程执行一个动作,并且还可以在COnd上等待渲染线程完成该动作。渲染线程在Cond上等待主线程发送渲染请求,并使用相同的Cond告诉主线程完成了必要的操作。我得到的错误发生在渲染线程试图锁定Cond以检查/等待渲染请求时,此时它不应该被锁定(因为主线程正在等待它),更不用说由同一个线程。下面是输出:

DEBUG: Created window 
DEBUG: OpenGL 3.0 Mesa 9.1.4, GLSL 1.30 
DEBUG: setScreen locking from thread 140564696819520 
DEBUG: Locking Cond 0x13ec1e0 in thread 140564696819520 
DEBUG: Releasing Cond 0x13ec1e0 in thread 140564696819520 
DEBUG: Entering GLFW main loop 
DEBUG: requestRender locking from thread 140564696819520 
DEBUG: Locking Cond 0x13ec1e0 in thread 140564696819520 
DEBUG: requestRender waiting 
DEBUG: Cond 0x13ec1e0 waiting in thread 140564696819520 
DEBUG: Running thread 'RenderThread' with id 140564575180544 
DEBUG: render thread::run locking from thread 140564575180544 
DEBUG: Locking Cond 0x13ec1e0 in thread 140564575180544 
terminate called after throwing an instance of 'std::system_error' 
    what(): Resource deadlock avoided 

说实话,我真的不明白一个unique_lock是什么,为什么需要condition_variable一个,而不是直接使用互斥体,所以这可能是问题的原因。我无法在网上找到很好的解释。

+3

不要为所有线程使用相同的'unique_lock',这并不意味着它会被使用。将它们用作块范围内的RAII对象,而不是类成员。这样,每个调用函数的线程都将拥有自己的实例。另外,介绍虚假唤醒。 – syam

+0

我看到了,所以想要等待或发送通知的每个上下文应该使用它自己的unique_lock,但是所有共享相同的互斥量? – realh

+0

只需等待,不发送('cv.notify()'不需要锁)。但是,否则,是的。我会尽量整理一个答案,告诉你如何正确使用这一点,现在我只是有点忙。 – syam

回答

7

前言:要理解条件变量,重要的是他们可能会受到随机的,虚假的唤醒。换句话说,简历可以从wait()退出,没有任何人先拨打notify_*()。不幸的是,没有办法区分这种虚假的唤醒与合法的唤醒,所以唯一的解决方案是有一个额外的资源(至少是一个布尔值),以便您可以判断唤醒条件是否实际满足。

这个额外的资源也应该由一个互斥体来保护,通常和你作为CV的同伴一样。


一个CV /互斥体对的典型用法如下:

std::mutex mutex; 
std::condition_variable cv; 
Resource resource; 

void produce() { 
    // note how the lock only protects the resource, not the notify() call 
    // in practice this makes little difference, you just get to release the 
    // lock a bit earlier which slightly improves concurrency 
    { 
     std::lock_guard<std::mutex> lock(mutex); // use the lightweight lock_guard 
     make_ready(resource); 
    } 
    // the point is: notify_*() don't require a locked mutex 
    cv.notify_one(); // or notify_all() 
} 

void consume() { 
    std::unique_lock<std::mutex> lock(mutex); 
    while (!is_ready(resource)) 
     cv.wait(lock); 
    // note how the lock still protects the resource, in order to exclude other threads 
    use(resource); 
} 

相比,你的代码,请注意线程如何将几个可以同时调用produce()/consume()而不必担心共享unique_lock:唯一共享事情是mutex/cv/resource,每个线程都有自己的unique_lock,如果互斥锁已被别的东西锁住,强制线程等待。

正如你所看到的,资源不能真正与CV/mutex对分开,这就是为什么我在评论中说你的包装类并不真正适合IMHO,因为它确实试图分离它们。

通常的做法不是像你试图的那样为CV/mutex对创建一个包装,而是整个CV/mutex /资源三重组的。例如。一个线程安全的消息队列,消费者线程将在CV上等待,直到队列中的消息准备好被消耗。


如果你真的想包装只是CV /互斥体对,你应该摆脱你的lock()/release()方法,这是不安全的(从一个RAII点),并用一个单一的lock()方法返回替换它们一个unique_ptr

std::unique_ptr<std::mutex> lock() { 
    return std::unique_ptr<std::mutex>(cMutex); 
} 

这样您就可以使用您的Cond包装类中,而相同的方式,就是我上面显示:

Cond cond; 
Resource resource; 

void produce() { 
    { 
     auto lock = cond.lock(); 
     make_ready(resource); 
    } 
    cond.notify(); // or notifyAll() 
} 

void consume() { 
    auto lock = cond.lock(); 
    while (!is_ready(resource)) 
     cond.wait(lock); 
    use(resource); 
} 

但说实话,我不确定它是否值得麻烦:如果您想使用recursive_mutex而不是简单的mutex?那么,你必须从你的类中创建一个模板,以便你可以选择互斥体类型(或者完全写出第二个类,以便代码复制)。无论如何,由于您仍然需要编写几乎相同的代码才能管理资源,因此收益并不大。一个仅用于CV /互斥对的包装类太薄了一个包装,真正有用的恕我直言。但像往常一样,YMMV。

+0

感谢您的详细解答。但是,在调用cv.wait()时,您是否必须传递对unique_lock的引用? – realh

+0

哎呀你是对的,看起来我有点被带走了。 :)现在解决这个问题。 – syam

+0

我使用包装类的原因是因为我正在编写一些可移植的框架代码。我最初将在Windows和Linux上使用SDL的线程API,并且在Android中将所有线程管理都留给Java或使用pthread。我并不是100%确信C++ 11支持在我可能愿意支持的每个平台上都很稳定,但希望它可以。 – realh