带有 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】:您可以同时使用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;
;
【讨论】:
CamIface 的生命周期不是问题,我将其添加到问题中。您的建议是一直运行一个线程进行清理,等待,并在停止为真时启动?好主意,谢谢! @eudoxos 是的,没有必要一次又一次地创建一个新线程。创建线程也需要付出代价。 我想我必须在stop()
中加入某种循环,否则它只会被调用一次。然后检查是否使用其他变量调用了 dtor,以知道何时返回?
@eudoxos 是的,这是一个错误。您应该有一个循环,并且还有一些机制可以在 CamIface 销毁时唤醒停止线程。我会更新答案。谢谢!【参考方案4】:
这很难正确做到这一点应该表明您的设计是奇怪的不对称。相反,将所有相机交互放在后台线程中,以及来自该线程的所有互斥操作。将相机线程视为拥有相机资源和相应的互斥锁。
然后使用 std::future 或其他同步(如并发队列)跨线程边界传递捕获的帧。您可以从这里考虑使后台线程持久化。请注意,这并不意味着捕获必须一直运行,它可能只是使线程管理更容易:如果相机对象拥有线程,析构函数可以发出信号退出,然后join()
它。
【讨论】:
以上是关于带有 RAII 的 std::mutex 但在后台线程中完成和释放的主要内容,如果未能解决你的问题,请参考以下文章
std::mutex 和 std::shared_mutex 之间的区别