在不同的函数中锁定/解锁 std::unique_lock

Posted

技术标签:

【中文标题】在不同的函数中锁定/解锁 std::unique_lock【英文标题】:Lock/unlock std::unique_lock in different functions 【发布时间】:2021-11-19 16:38:01 【问题描述】:

我正在编写一个使用条件变量的程序:

bool flag;
std::mutex mtx;
std::condition_variable cond;


  std::unique_lock<std::mutex> lock(mtx);
  cond.wait(lock, [&] return flag; );

  // ... do some stuff
  // ... do some more stuff

  flag = false;

 // lock unlocked here

现在我面临的问题是“做一些事情”和“做更多事情”实际上是在两个单独的回调函数中实现的,这些函数从其他地方一个接一个地调用,即:

void callback1() 
   std::unique_lock<std::mutex> lock(mtx);
   cond.wait(lock, [&] return flag; );

   // ... do some stuff
 // ERROR! lock unlocked too early

void callback2() 
   // ... do some more stuff
  flag = false;
 // lock should be unlocked here

我该如何解决这个问题?我可以将这两个回调封装在某个将锁作为成员变量保存的对象中,但在我看来,这违反了std::unique_lock 的预期语义。

编辑:更多上下文:

其他人编写了一个类Foo,如下所示:

class Foo

public:
  void bar() 
    before_bar_callback();

    // ...
    
    after_bar_callback();
  

private:
  virtual void before_bar_callback() 
  virtual void after_bar_callback() 
;

我无法修改Foo::bar,它在单独的线程中运行。我想等待before_bar_callback 中的条件变量,然后设置flag = false 并解锁after_bar_callback 中的关联锁。

【问题讨论】:

这些信息不足以得出一个平衡的答案。为什么是 2 个回调而不是 1 个。如果他们如此依赖,我认为他们应该合二为一。 @PKramer 我添加了更多上下文。 我想我知道你在做什么。如果我理解正确,您希望两个回调基本上都是原子的,因为您不想冒两者之间“无效”的风险。我参与了一个处理这个问题的项目,他们最终决定在回调之间解锁是可以的。否则你最终会陷入各种奇怪的死锁场景。现实情况是该程序的多线程并不足以导致错误(但它确实发生了)。在此之后,我认为唯一的解决方案是使用异步事件,这会带来其自身的挑战。 使用那个分割界面,你不能使用 RAII :/ 您可以使用std::condition_variable_any 并将mtx 直接传递给它的wait。这样callback1返回时不会自动解锁。 【参考方案1】:

根据您的信息,这是我最接近一种 RAII 方法的方法。 当一个类没有做你想做的事时,有时包装它会有所帮助。 这就是我能想到的。理想情况下,您仍然应该检查 Foo 是否仍不能被“所有者”修改。

想法:在 Foo 有机会完成它的工作并调用你的回调之前获取一个锁。这样你就知道锁在你的回调中是活跃的。

#include <functional>
#include <mutex>
#include <iostream>
#include <condition_variable>

//-------------------------------------------------------------------------
// your stand in for Foo,
// the class you cannot change

class  Foo 

public:
    Foo() = default;
    virtual ~Foo() = default;

    void bar()
    
        // todo construct a context here
        before_callback();
        // work here
        after_callback();
        // and destroy it here.
    

private:
    virtual void before_callback() ;
    virtual void after_callback() ;

;

//-------------------------------------------------------------------------
// A class that should look like Foo but wraps it
// Foo is a member and FooWrapper can intercept calls
// to Foo and do extra things.

class MyFoo :
    protected Foo

public:
    MyFoo() = default;
    virtual ~MyFoo() = default;

    // shadows base classes bar intentionally
    void bar() 
    
        std::unique_lock<std::mutex> lock m_mtx ;
        m_lock = &lock;
        Foo::bar();
    ;

    void before_callback() override
    
        //lock is active
        m_cv.wait(*m_lock, [this] return m_flag; );
        std::cout << "before_callback" << std::endl;
    

    void after_callback() override
    
        //lock is active
        std::cout << "after_callback" << std::endl;
    

private:
    bool m_flag true ;
    std::condition_variable m_cv;
    std::unique_lock<std::mutex>* m_lock nullptr ;
    std::mutex m_mtx;
;

int main()

    MyFoo foo;
    foo.bar();

    return 0;

【讨论】:

据我所知,Foo 是一个“接口”,因此无法包装它。然后你必须正常使用继承。 (如果你把它包装起来before_callback/after_callback 无论如何都是无关紧要的,因为在调用m_foo.bar() 之前可能会获得锁)。 @Jarod42 接口可以是包装器,我同意回调不必在包装器中,只要它们可以访问锁的(指针)即可。无论如何,我真的不喜欢依赖解决方法(包括这个)。多年来我了解到,解决真正的问题(设计)会更好,即使这意味着要去找我使用的代码的供应商。 我的意思是,正如代码所示,我知道它应该有一个您无法更改的lib_function(Foo&amp;);(如Foo)。 我现在看到 OP 暗示必须覆盖 before_callback/after_callback(即使他将虚拟函数设为私有)。然后一个虚拟派生类可以覆盖这些方法,并且仍然提供一个新的 bar 实现,该实现锁定并转发到 Foo::bar()。但是是的,如果它是一个 lib_function(Foo&) 事情看起来就不那么好了 @PKramer 是的,覆盖是想法(并且仍然适用于私有成员函数)。

以上是关于在不同的函数中锁定/解锁 std::unique_lock的主要内容,如果未能解决你的问题,请参考以下文章

stdthread并发unique_lock

std::unique_lock 移动语义

如何在 C++ 中使用 std::unique_ptr?

C++ 使用带有基类的已删除函数 std::unique_ptr

如何在构造函数中使用删除器初始化 std::unique_ptr?

为什么以不同的顺序解锁两个锁定的银行账户会导致死锁?