带有 RAII 的 std::mutex 但在后台线程中完成和释放

Posted

技术标签:

【中文标题】带有 RAII 的 std::mutex 但在后台线程中完成和释放【英文标题】:std::mutex with RAII but finish & release in background thread 【发布时间】:2016-09-14 06:53:32 【问题描述】:

我有一个偶尔从 GigE 相机获取帧的功能,并希望它快速返回。标准流程是这样的:

// ...
camera.StartCapture();
Image img=camera.GetNextFrame();
camera.StopCapture(); // <--  takes a few secs
return img;

GetNextFrame()StopCapture() 之后返回数据准备好,速度很慢;因此,我想尽快返回img 并生成一个后台线程来执行StopCapture()。但是,在(不太可能)再次开始获取的情况下,我想保护互斥锁的访问。有些地方可能会引发异常,因此我决定使用 RAII 样式的锁,它将在范围退出时释放。同时,我需要将锁转移到后台线程。像这样的东西(伪代码):

class CamIface
   std::mutex mutex;
   CameraHw camera;
public:
   Image acquire()
      std::unique_lock<std::mutex> lock(mutex); // waits for cleanup after the previous call to finish
      camera.StartCapture();
      Image img=camera.GetNextFrame();
      std::thread bg([&]
         camera.StopCapture(); // takes a long time
         lock.release(); // release the lock here, somehow
       );
       bg.detach();
       return img;
       // do not destroy&release lock here, do it in the bg thread
   ;

;

如何将锁从调用者转移到产生的后台线程?或者有没有更好的方法来处理这个问题?

编辑:保证CamIface 实例有足够的生命周期,请假设它永远存在。

【问题讨论】:

我会将线程附加到CamIface 而不是分离它。 ***.com/a/20669290/104774 应该回答你的问题,虽然我更喜欢@PeterT 的回答 我认为它不像移动捕捉那么简单。 std::mutex::unlock 必须在互斥锁被锁定的同一线程上调用:en.cppreference.com/w/cpp/thread/mutex/unlock Tangential - 在这个线程被调度和这个对象可能被破坏之间存在竞争条件。当lambda开始执行时,通过引用捕获的this可能指向释放的内存,访问camera是UB。如果 StopCapture 是硬件正确停止的重要调用,请考虑使用 enable_shared_from_this 并捕获 std::shared_ptr 以使对象保持活动状态。 很难正确地做到这一点的事实应该表明你的设计是奇怪的不对称。而是将所有相机交互放在后台线程中,以及来自该线程的所有互斥操作。然后只需使用 std::future 或其他简单同步将捕获的帧传递到线程边界。您可以从这里考虑使后台线程持久化,甚至永远不会停止捕获。 【参考方案1】:

更新答案: @Revolver_Ocelot 是对的,我的回答鼓励了未定义的行为,我想避免这种行为。

所以让我使用来自this SO Answer 的简单信号量实现

#include <mutex>
#include <thread>
#include <condition_variable>

class Semaphore 
public:
    Semaphore (int count_ = 0)
        : count(count_) 

    inline void notify()
    
        std::unique_lock<std::mutex> lock(mtx);
        count++;
        cv.notify_one();
    

    inline void wait()
    
        std::unique_lock<std::mutex> lock(mtx);

        while(count == 0)
            cv.wait(lock);
        
        count--;
    

private:
    std::mutex mtx;
    std::condition_variable cv;
    int count;
;


class SemGuard

    Semaphore* sem;
public:
    SemGuard(Semaphore& semaphore) : sem(&semaphore)
    
        sem->wait();
    
    ~SemGuard()
    
        if (sem)sem->notify();
    
    SemGuard(const SemGuard& other) = delete;
    SemGuard& operator=(const SemGuard& other) = delete;
    SemGuard(SemGuard&& other) : sem(other.sem)
    
        other.sem = nullptr;
    
    SemGuard& operator=(SemGuard&& other)
    
        if (sem)sem->notify();
        sem = other.sem;
        other.sem = nullptr;
        return *this;
    
;

class CamIface
   Semaphore sem;
   CameraHw camera;
public:
   CamIface() : sem(1)
   Image acquire()
      SemGuard guard(sem);
      camera.StartCapture();
      Image img=camera.GetNextFrame();
      std::thread bg([&](SemGuard guard)
         camera.StopCapture(); // takes a long time
       , std::move(guard));
       bg.detach();
       return img;
   ;

;

旧答案: 就像 PanicSheep 说的,将互斥体移到线程中。比如这样:

std::mutex mut;

void func()

    std::unique_lock<std::mutex> lock(mut);
    std::thread bg([&](std::unique_lock<std::mutex> lock)
    
         camera.StopCapture(); // takes a long time
    ,std::move(lock));
    bg.detach();

另外,请注意,不要这样做

std::thread bg([&]()

     std::unique_lock<std::mutex> local_lock = std::move(lock);
     camera.StopCapture(); // takes a long time
     local_lock.release(); // release the lock here, somehow
);

因为您正在加速线程启动和函数作用域结束。

【讨论】:

互斥锁所有权是线程的属性。您必须在获得它的同一线程中解锁互斥锁。 Otherwise you have UB 是的,我想信号量或其他什么都合适 @Revolver_Ocelot 再次感谢您的评论,我添加了一个使用简单信号量的版本。 (编辑:但我忘记了例外要求,我要去看看) 如果抛出异常,semphore 将如何表现?如果GetNextFrame 抛出,则永远不会调用 notify。 @Revolver_Ocelot:单线程规则对共享互斥体也有效吗?我可以在获取开始时要求独占锁,然后降级为共享(在线程返回和清理线程之间); boost::thread 有这个(unlock_and_lock_shared),不确定 std::thread。【参考方案2】:

将 std::unique_lock 移至后台线程。

【讨论】:

是的,这就是我想做的。怎么样? 警告:如果调用 CamIFace 实例(拥有互斥锁)超出其自身线程的范围并在进程中破坏互斥锁,则将无济于事 - 请确保您让它活着。【参考方案3】:

您可以同时使用mutexcondition_variable 进行同步。分离后台线程也很危险,因为当 CamIface 对象被破坏时,线程可能仍在运行。

class CamIface 
public:
    CamIface() 
        background_thread = std::thread(&CamIface::stop, this);
    
    ~CamIface() 
        if (background_thread.joinable()) 
            exit = true;
            cv.notify_all();
            background_thread.join();
        
    
    Image acquire() 
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [this]()  return !this->stopping; );
        // acquire your image here...
        stopping = true;
        cv.notify_all();
        return img;
    
private:
    void stop() 
        while (true) 
            std::unique_lock<std::mutex> lock(mtx);
            cv.wait(lock, [this]()  return this->stopping || this->exit; );

            if (exit) return;   // exit if needed.

            camera.StopCapture();
            stopping = false;
            cv.notify_one();
        
    

    std::mutex mtx;
    std::condition_variable cv;
    atomic<bool> stopping = false;
    atomic<bool> exit = false;
    CameraHw camera;
    std::thread background_thread;
;

【讨论】:

CamIface 的生命周期不是问题,我将其添加到问题中。您的建议是一直运行一个线程进行清理,等待,并在停止为真时启动?好主意,谢谢! @eudoxos 是的,没有必要一次又一次地创建一个新线程。创建线程也需要付出代价。 我想我必须在stop() 中加入某种循环,否则它只会被调用一次。然后检查是否使用其他变量调用了 dtor,以知道何时返回? @eudoxos 是的,这是一个错误。您应该有一个循环,并且还有一些机制可以在 CamIface 销毁时唤醒停止线程。我会更新答案。谢谢!【参考方案4】:

这很难正确做到这一点应该表明您的设计是奇怪的不对称。相反,将所有相机交互放在后台线程中,以及来自该线程的所有互斥操作。将相机线程视为拥有相机资源和相应的互斥锁。

然后使用 std::future 或其他同步(如并发队列)跨线程边界传递捕获的帧。您可以从这里考虑使后台线程持久化。请注意,这并不意味着捕获必须一直运行,它可能只是使线程管理更容易:如果相机对象拥有线程,析构函数可以发出信号退出,然后join() 它。

【讨论】:

以上是关于带有 RAII 的 std::mutex 但在后台线程中完成和释放的主要内容,如果未能解决你的问题,请参考以下文章

死锁使用 std::mutex 保护多线程中的 cout

二.共享数据的保护

std::mutex 和 std::shared_mutex 之间的区别

std::mutex 锁定函数和 std::lock_guard<std::mutex> 的区别?

C++11 并发指南三(std::mutex 详解)

为啥 OSX 上的 std::mutex 这么慢?