c++ - 提供对不同类型数据的线程安全访问的类(提案、代码审查)
问题描述
仍然处于初学者水平,我目前正在用 C++ 为 raspi 4 编写一个多线程应用程序,该应用程序对来自飞行时间深度相机的帧执行一系列操作。
情况
我的线程以及来自深度相机库的回调会产生各种数据(从 bool 到更复杂的类型,如 opencv mats 等)。我想在一个地方收集一些相关数据,然后不时通过 UDP 将其发送到智能手机监控应用程序,让我可以监控线程的行为......
我无法控制线程何时访问部分数据,我也不能保证它们不会同时访问它。因此,我寻找了一种方法,使我能够将数据写入和读取到结构中,而完全不必担心线程安全性。但到目前为止,我找不到满足我需求的好的解决方案。
“不要使用全局变量”
我知道这是一个类似于全局的概念,如果可能的话,应该避免它。由于这是某种日志记录/监控,我会认为它是一个跨领域问题并以这种方式管理它......
代码/提案
所以我想出了这个,我很高兴看到并发专家对它进行评论:
您也可以在此处在线运行代码!
#include <chrono>
#include <iostream>
#include <mutex>
#include <thread>
// Class that provides a thread-safe / protected data struct -> "ProtData"
class ProtData {
private:
// Struct to store data.
// Core concern: How can I access this in a thread-safe manner?
struct Data {
int testInt;
bool testBool;
// OpenCV::Mat (CV_8UC1)
// ... and a lot more types
};
Data _data; // Here my data gets stored
std::mutex _mutex; // private mutex to achieve protection
// As long it is in scope this protecting wrapper keeps the mutex locked
// and provides a public way to access the data structure
class ProtectingWrapper {
public:
ProtectingWrapper(Data& data, std::mutex& mutex)
: data(data), _lock(mutex) {}
Data& data;
std::unique_lock<std::mutex> _lock;
};
public:
// public function to return an instance of this protecting wrapper
ProtectingWrapper getAccess();
};
// public function to return an instance of this protecting wrapper
ProtData::ProtectingWrapper ProtData::getAccess() {
return ProtectingWrapper(_data, _mutex);
}
// Thread Function:
// access member of given ProtData after given time in a thread-safe manner
void waitAndEditStruct(ProtData* pd, int waitingDur, int val) {
std::cout << "Start thread and wait\n";
// wait some time
std::this_thread::sleep_for(std::chrono::milliseconds(waitingDur));
// thread-safely access testInt by calling getAccess()
pd->getAccess().data.testInt = val;
std::cout << "Edit has been done\n";
}
int main() {
// Instace of protected data struct
ProtData protData;
// Two threads concurrently accessing testInt after 100ms
std::thread thr1(waitAndEditStruct, &protData, 100, 50);
std::thread thr2(waitAndEditStruct, &protData, 100, 60);
thr1.join();
thr2.join();
// access and print testInt in a thread-safe manner
std::cout << "testInt is: " << protData.getAccess().data.testInt << "\n";
// Intended: Errors while accessing private objects:
// std::cout << "this won't work: " << protData._data.testInt << "\n";
// Or:
// auto wontWork = protData.ProtectingWrapper(/*data obj*/, /*mutex obj*/);
// std::cout << "won't work as well: " << wontWork.data.testInt << "\n";
return 0;
}
问题
所以考虑到这段代码,我现在可以从任何地方访问结构的变量protData.getAccess().data.testInt
。
- 但它真的是线程安全的吗?
- 你会认为这个类是“好代码”(性能、可读性)吗?
我尽力使代码易于理解。如果您有任何问题,请写评论,我会尝试更深入地解释它......
提前致谢
解决方案
不,这不是线程安全的。考虑:
ProtData::Data& data_ref = pd->getAccess().data;
现在我有了对数据的引用,并且在创建 a 时锁定的互斥体ProtectingWrapper
已经解锁,因为临时包装器已经消失了。即使是const
引用也无法解决这个问题,因为那时我可以从该引用中读取,而另一个线程写入data
.
我的经验法则是:不要让引用(无论是否const
)泄漏出锁定的范围。
你会认为这个类是“好代码”(性能、可读性)吗?
这是非常基于opinin的。尽管您应该考虑到同步并不是您想在所有地方都使用的东西,而只是在必要时才使用。在您的示例中,您可以修改testInt
and testBool
,但要这样做,您需要锁定同一个互斥锁两次。如果您的班级有许多需要同步的成员,那么情况会变得更糟。考虑一下这个,它更简单且不能被滥用:
template <typename T>
struct locked_access {
private:
T data;
std::mutex m;
public:
void set(const T& t) {
std::unique_lock<std::mutex> lock(m);
data = t;
}
T get() {
std::unique_lock<std::mutex> lock(m);
return data;
}
};
但是,即使这样,我也可能不会使用,因为它无法扩展。如果我有一个包含两个locked_access
成员的类型,那么我会回到第 1 步:我想详细控制我是只修改其中一个成员还是同时修改两个成员。我知道编写线程安全的包装器很诱人,但根据我的经验,它只是无法扩展。相反,线程安全需要融入到类型的设计中。
PS:您Data
在 的私有部分中声明ProtData
,但是一旦可以通过公共方法访问的实例Data
,该类型也可以访问。只有类型的名称是私有的。我应该auto
在上面的行中使用它,但我更喜欢这样,因为它更清楚发生了什么。
推荐阅读
- python - 在 python 中使用 plt.subplots 清理代码
- react-native - 使用此包 REACT NATIVE 的功能时应用程序崩溃
- python-3.x - Win 10 路径变量被搞砸了
- docusignapi - DocuSign API - 如何实现“需要查看”
- android - 使用 OkHttp3 在代理上拒绝 Android 连接
- jpackage - 使用 --app-image 选项创建安装程序时 jpackage 崩溃
- sql - ActiveRecords,我通过提供其他表的 id 过滤值
- python - 在不加载完整内容的情况下找出 csv 中行数的最佳方法
- typescript - 确保 TypeScript 中的对象类型
- mysql - 我的 python mysql 文件查询在哪里更新?MySQL 8.0 命令行客户端中未显示更新