访问类私有的大量数据的正确方法是什么?

What is the correct way to access a large amount of data that is private to a class?

本文关键字:方法 是什么 数据 访问      更新时间:2023-10-16

我只是想知道如何解决这个与C98(所以没有shared_ptr):

  • 我有一个非常大的类,有很多数据(BigData)
  • 我有一个类DataStorage,它在map
  • 中跟踪该数据
  • 数据不太可能改变,但可以
  • 我在多线程环境中

     class BigData;
     class DataStorage{
      public:
           const BigData *getStuff(int which_one) const{
              lock_guard<mutex> guard(mut);     
               return &myReallyBigDatas[which_one]; // thanks Donghui
           }
      protected:
         mutable mutex mut;
         map<int,BigData> myReallyBigDatas;
     }
    

(正如Keith m建议的那样)

我知道这段代码是错误的,从我的角度来看,我想解决两个主要问题:

  1. 如果对象返回消失,因为这个位置在地图上被删除或覆盖(我将有一个指针指向没有地方,UB!)
  2. 如果返回的BigData实例被修改

当然,我想找到一个解决方案,以避免其他人更改此代码的错误

我想出了这些解决方案:

  1. 为BigData添加互斥锁。这就解决了问题2
  2. 改变函数返回一个真正的对象,而不是一个指针(这是非常好的,但有杀死性能的缺点,而它是一个真正的大类的副本)这解决了两个问题
  3. 实现我自己的shared_ptr类(不能使用C11或boost)。这解决了问题1
  4. 在DataStorage类上创建一个锁/解锁(真的很糟糕!)这两个问题都解决了
  5. 保持错误,多祈祷。这个…

我很确定很多人都在遗留代码中发现了这样的一段代码,我想找到最好的解决方案。

注:我知道我用的是C11互斥锁。我的实际代码没有它,但是这样编写示例代码更容易。

实际上,shared_ptrmutex是相互独立的,您可能需要两者- shared_ptr用于保证只有一个资源释放,而mutex用于保证没有并发读/写操作(或者也可以并发读,取决于互斥锁的类型)。

使用shared_ptr基本上意味着没有单一的数据所有者。虽然这是可能的管理(例如,引用计数),这并不总是最好的解决方案(记得关于循环依赖,需要weak_ptr等等)——有时是更好的找出一个所有者的资源将负责回收的时候它不再是必要的(例如,如果你有一个工作线程池,它可能产生的其他)——当然,你必须保证它会比别人活得更长,让每个人都能访问这些数据。因此,您有几个选项来管理对象的生命周期:

  • 从boost/c++ 11标准库/Loki/some-other-existing-implementation中"借用"代码(检查许可证以验证是否可以使用它们),而不使用整个库-您可能需要对它们进行更改
  • 写你自己的智能指针-硬的,只有专业人士-完全不推荐
  • 选择资源的单个所有者-这是我建议的

当涉及到访问冲突管理时,基本上有两种方法:

  • 使用某种锁来管理它们(我假设你有一个可用的)
  • 通过只允许一个线程写对象来避免它们。其他可能通常想要的将不得不从"所有者线程"请求写。这种方法非常适合单一资源所有者,但它更像是一个参与者模型,而不是典型的共享内存多线程,因此可能很难在遗留应用程序中引入。

您可以将锁与任何树内存管理方法一起使用,单写入器方法与单所有者方法最适合。请注意,这是范式的一个重大变化,可能需要做大量的工作来实现消息队列和工作者之类的东西。

如果你已经有了基础设施(队列,工人等),我建议考虑单所有者,单写入方法,否则带锁的单所有者可能是一个很好的解决方案。如果你不能选择一个单一的所有者,从现有的库中提取代码——不要自己写,因为你会犯一些错误,多线程环境中的内存管理是非常困难的

编辑1

既然你已经澄清了问题,我觉得这个答案有点太高级了,所以我要再补充一些细节。

你能做的最简单的事情就是返回BigData&而不是BigData*——没有人应该删除它(当然这是可能的,但实际上所有东西都是在c++中)。否则,您还可以:

  • 只允许一个线程使用一个BigData实例-(例如,通过存储额外的std::map<int, thread_id>与使用BigData的信息-只有当你不需要从多个线程并发访问同一个实例
  • 返回类似BigDataProxy的东西,而不是BigData——Proxy应该有一个特殊的功能来删除资源,然后由"最后一个感兴趣的人"执行——这实际上只是shared_ptr的一个特殊情况,但可能更容易实现。
从概念上讲,Proxy应该是这样的(伪代码-忽略私有/公共成员等):
class BigDataProxy {
  public:
  BigDataProxy(data_, instanceId_): data(data_), instanceId(instanceId_) {
    std::lock_guard l(data.mutex);
    data.interestedThreads[instanceId].insert(this_thread::thread_id);
  }
  ~BigDataProxy() {
    std::lock_guard l(data.mutex);
    data.interestedThreads[instanceId].remove(this_thread::thread_id)
    if(data.interestedThreads[instanceId].empty() && data.toDelete.contains(instanceId) {
      data.elems.remove(instanceId);
      data.toDelete.remove(instanceId);
    }
  }
  BigData& operator*() {
    return data.elems[instanceId];
  }
  void remove() {
     std::lock_guard l(data.mutex);
     data.toDelete.add(instanceId);
  }
  private: 
    DataStorage& data;
    int instanceId;
}

DataStorage中进行了更改,要求它看起来像这样:

class DataStorage {
  std::mutex mutex;
  std::map<int, BigData> elems;
  std::set<int> toDelete;
  std::map<int, std::set<thread_id> > interested_threads;
}

注意这是伪代码,异常处理在这里会很困难。

你的想法是正确的:保持数据的私密性,并提供一个getter来获取数据或部分数据。

你的代码有一个错误。getStuff()的签名返回一个指向BigData的指针,但是实现返回一个指向BigData的引用。类型不匹配。

你是对的,你不想复制BigData。所以你有三个选择:

  • 返回myReallyBigDatas [which_one];//返回BigData&
  • 返回,myReallyBigData [which_one];//返回BigData*
  • 返回myReallyBigDatas.find (which_one);//返回map::iterator