首页 > 解决方案 > 提供对不同类型数据的线程安全访问的类(提案、代码审查)

问题描述

仍然处于初学者水平,我目前正在用 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

我尽力使代码易于理解。如果您有任何问题,请写评论,我会尝试更深入地解释它......

提前致谢

标签: c++structconcurrencyraspberry-pithread-safety

解决方案


不,这不是线程安全的。考虑:

ProtData::Data& data_ref = pd->getAccess().data;

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

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

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

这是非常基于opinin的。尽管您应该考虑到同步并不是您想在所有地方都使用的东西,而只是在必要时才使用。在您的示例中,您可以修改testIntand 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在上面的行中使用它,但我更喜欢这样,因为它更清楚发生了什么。


推荐阅读