问题 std :: mutex with RAII但在后台线程中完成并释放


我有一个偶尔从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 实例是确定的,请假设它永远存在。


8741
2017-09-14 06:53


起源

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


答案:


这很难做到的事实应该表明你的设计是奇怪的不对称。相反,将所有相机交互放在后台线程中,并使用该线程中的所有互斥操作。将相机线程视为拥有相机资源和相应的互斥锁。

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


1
2017-09-26 12:45





更新答案: @Revolver_Ocelot是正确的,我的回答鼓励未定义的行为,我想避免。

所以让我使用简单的Semaphore实现 这个SO答案

#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
});

因为你正在竞争线程启动和功能范围的结束。


7
2017-09-14 07:20



互斥锁所有权是线程的属性。您 必须 在你获得的同一个线程中解锁互斥锁。 否则你有UB - Revolver_Ocelot
是的,我猜一个信号量或其他东西,是合适的 - PeterT
@Revolver_Ocelot再次感谢评论,我使用一个简单的信号量添加了一个版本。 (编辑:但我忘记了异常要求,我将会看到这个) - PeterT
如果抛出异常,旁观者将如何表现?如果 GetNextFrame 抛出,通知将永远不会被调用。 - eudoxos
@Revolver_Ocelot:单线程规则对共享互斥锁也有效吗?我可以在获取开始时请求独占锁定,然后降级到共享(在线程返回和清理线程之间); boost :: thread有这个(unlock_and_lock_shared),不确定std :: thread。 - eudoxos


将std :: unique_lock移动到后台线程。


3
2017-09-14 07:08



是的,这就是我想要做的。怎么样? - eudoxos
警告:如果调用的CamIFace实例(拥有互斥锁)超出了自己的线程范围而破坏了进程中的互斥锁,那将无济于事 - 请确保将其保持活动状态。 - Adrian Colomitchi


你可以使用两者 mutex 和 condition_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;
};

3
2017-09-14 07:52



CamIface的生命周期不是问题,我在问题中补充说。你建议运行一个线程一直进行清理,等待,并在停止时启动?好主意,谢谢! - eudoxos
@eudoxos是的,没有必要一次又一次地创建一个新的线程。创建线程也需要它的价格。 - for_stack
我想我必须把某种循环放进去 stop(),否则它只会被调用一次。然后检查是否使用其他变量调用dtor,以了解何时返回? - eudoxos
@eudoxos是的,这是一个错误。你应该有一个循环,并且还有一些机制可以在CamIface破坏时唤醒停止线程。我会更新答案。谢谢! - for_stack