提问者:小点点

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


我现在还是初学者,正在用C++为Raspi4编写一个多线程应用程序,它对来自飞行时间深度相机的帧执行一系列操作。

我的线程以及来自depth-camera-library的回调生成各种类型的数据(从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从任何地方访问结构的变量。

  • 但是它真的是线程安全的吗?
  • 您认为这个类是“好代码”吗(性能,可读性)?

我尽了最大的努力使代码变得易懂。 如果你有任何问题,请写一个评论,我会尽量解释得更深入。

提前致谢


共1个答案

匿名用户

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

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

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

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

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

这是非常基于Opinin的。 尽管您应该考虑到同步并不是所有地方都要使用的东西,而是只在必要的时候使用。 在您的示例中,您可以修改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,但我更喜欢用这种方式,因为它更清楚地说明了正在发生的事情。