提供对不同类型的数据(建议、代码审查)的线程安全访问的类

Class that provides thread-safe access to data of different types (Proposal, Code review)

本文关键字:代码审查 线程 安全 访问 同类型 数据 建议      更新时间:2023-10-16

仍然是初学者,我目前正在为 C++ 中的 raspi 4 编写一个多线程应用程序,该应用程序对来自飞行时间深度相机的帧执行一系列操作。

情况

我的线程以及来自深度相机库的回调会产生各种数据(从布尔值到更复杂的类型,如 opencv 垫等(。我想在一个地方收集一些相关数据,然后不时通过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 waitn";
// 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 donen";
}
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;

现在我有一个对数据的引用,并且在创建ProtectingWrapper时锁定的互斥锁已经解锁,因为临时包装器已经消失了。即使是const引用也无法解决这个问题,因为这样我就可以从该引用中读取,而不同的线程写入data.

我的经验法则是:不要让引用(无论是否const(泄漏到锁定的范围之外。

你会认为这个类是"好代码"(性能、可读性(吗?

这是非常基于意见的。尽管您应该考虑到同步并不是您想要到处使用的,而是仅在必要时使用。在您的示例中,您可以修改testInttestBool,但要这样做,您需要锁定同一个互斥锁两次。如果你有一个包含许多需要同步的成员的类,那么情况可能会变得更糟。考虑一下,它更简单且不能滥用:

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:您在ProtData的私有部分中声明了Data,但是一旦可以通过公共方法访问Data的实例,该类型也可以访问。只有类型的名称是私有的。我应该在上面的行中使用auto,但我更喜欢这样,因为它更清楚地说明了正在发生的事情。