访问 class 私有的大量数据的正确方法是什么?
What is the correct way to access a large amount of data that is private to a class?
我只是想知道如何用 C98 解决这个问题(所以没有 shared_ptr
):
- 我有一个很大的 class 有很多数据 (
BigData
)
- 我有一个 class
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 所建议的。我没有提到我试图解决的问题)
我知道这段代码是错误的,从我的角度来看,我想解决两个主要问题:
- 如果对象 return 因为地图上的这个位置被删除或覆盖而消失(我会有一个指针指向任何地方,UB!)
- 如果 BigData 的实例 returned 被修改
当然,我想找到一个解决方案,避免将来其他人更改此代码的错误
我想到了这些解决方案:
- 在 BigData 中包含一个互斥体。这解决了问题 2
- 将函数更改为 return 一个真正的对象,而不是指针(这非常好,但缺点是在复制非常大的 class 时会降低性能)这解决这两个问题
- 实现我自己的 shared_ptr class(不能使用 C11 或 boost)。这解决了问题 1
- 在 class DataStorage 上创建锁定/解锁(真的很糟糕!)。这解决了两个问题
- 保持错误并多多祈祷。这个....
我很确定很多人在遗留代码中找到了这样一段代码,我想找到最好的解决方案。
P.S。我知道我正在使用 C11 互斥锁。我的真实代码没有,但是这样写示例代码更容易。
你的想法是正确的:保持数据隐私,并提供一个getter获取数据或部分数据。
您的代码中有错误。 getStuff() 的签名 return 是指向 BigData 的指针,但实现 return 是对 BigData 的引用。类型不匹配。
您是对的,因为您不想制作 BigData 的副本。所以你有三个选择:
- return myReallyBigDatas[which_one]; // return 大数据&
- return &myReallyBigData[which_one]; // return 大数据*
- returnmyReallyBigDatas.find(which_one); // return map::iterator
实际上,shared_ptr
和 mutex
是完全独立的,您可能需要两者 - shared_ptr
用于保证只释放一次资源,而 mutex
用于保证没有并发 [=78=] 操作(或并发读取,取决于互斥类型)。
使用shared_ptr
基本上意味着没有数据的单一所有者。虽然这是可以管理的(例如引用计数),但它并不总是最好的解决方案(记住循环依赖、需要 weak_ptr
等)——有时最好找出资源的单一所有者,谁将负责在不再需要时释放它(例如,如果你有一个工作线程池,它可能是产生其他线程的线程) - 当然,你必须保证它比其他人活得更久,以使每个人都可以访问数据。所以你有几个选项来管理对象的生命周期:
- "borrow" 来自 boost/C++11 标准 library/Loki/some-other-existing-implementation 的代码(检查许可证以验证您是否可以使用它们)而不使用整个库 - 您可能需要对它们进行更改
- 写你自己的智能指针 - 很难而且只为专业人士 - 完全不推荐
- select 资源的单一所有者 - 这是我推荐的
谈到访问冲突管理,基本上有两种方法:
- 使用某种锁来管理它们(我假设你有一把锁)
- 通过只允许一个线程写入对象来避免它们。其他通常可能想要的将不得不请求 "owner thread" 的写入。这种方法非常适合单个资源所有者,但它更像是一个参与者模型,而不是典型的共享内存多线程,因此可能很难在遗留应用程序中引入。
您可以将锁与任何树内存管理方法一起使用,单作者方法最适合单所有者。请注意,这是范例的重大变化,可能需要大量工作来实现诸如消息队列和工作人员之类的东西。
如果您已经拥有基础设施(队列、工作人员等),我建议您考虑单一所有者、单一作者方法,否则带锁的单一所有者可能是一个很好的解决方案。如果您无法 select 单一所有者,请从现有库中提取代码 - 不要自己编写,因为您会犯一些错误,多线程环境中的内存管理确实 很难
编辑 1
既然你把问题说清楚了,我觉得回答有点太高了,我再补充一些细节。
你能做的最简单的事情是 return 一个 BigData&
而不是 BigData*
- 那时没有人应该删除它(当然这是可能的,但实际上一切都在 C++ 中) .否则,您还可以:
- 仅允许单个线程使用单个
BigData
实例 -(例如通过存储额外的 std::map<int, thread_id>
以及有关已使用 BigData
的信息 - 仅当您不需要并发访问时从多个线程到同一个实例
- return 类似
BigDataProxy
而不是 BigData
- Proxy
应该有一个特殊的资源移除函数,然后由 [=77 执行=] - 这实际上只是 shared_ptr
的特例,但实现起来可能更简单。
从概念上讲,Proxy
类似于(伪代码 - 忽略 private/public 成员等):
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;
}
请注意,这是伪代码,这里的异常处理会很困难。
我只是想知道如何用 C98 解决这个问题(所以没有 shared_ptr
):
- 我有一个很大的 class 有很多数据 (
BigData
) - 我有一个 class
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 所建议的。我没有提到我试图解决的问题)
我知道这段代码是错误的,从我的角度来看,我想解决两个主要问题:
- 如果对象 return 因为地图上的这个位置被删除或覆盖而消失(我会有一个指针指向任何地方,UB!)
- 如果 BigData 的实例 returned 被修改
当然,我想找到一个解决方案,避免将来其他人更改此代码的错误
我想到了这些解决方案:
- 在 BigData 中包含一个互斥体。这解决了问题 2
- 将函数更改为 return 一个真正的对象,而不是指针(这非常好,但缺点是在复制非常大的 class 时会降低性能)这解决这两个问题
- 实现我自己的 shared_ptr class(不能使用 C11 或 boost)。这解决了问题 1
- 在 class DataStorage 上创建锁定/解锁(真的很糟糕!)。这解决了两个问题
- 保持错误并多多祈祷。这个....
我很确定很多人在遗留代码中找到了这样一段代码,我想找到最好的解决方案。
P.S。我知道我正在使用 C11 互斥锁。我的真实代码没有,但是这样写示例代码更容易。
你的想法是正确的:保持数据隐私,并提供一个getter获取数据或部分数据。
您的代码中有错误。 getStuff() 的签名 return 是指向 BigData 的指针,但实现 return 是对 BigData 的引用。类型不匹配。
您是对的,因为您不想制作 BigData 的副本。所以你有三个选择:
- return myReallyBigDatas[which_one]; // return 大数据&
- return &myReallyBigData[which_one]; // return 大数据*
- returnmyReallyBigDatas.find(which_one); // return map::iterator
实际上,shared_ptr
和 mutex
是完全独立的,您可能需要两者 - shared_ptr
用于保证只释放一次资源,而 mutex
用于保证没有并发 [=78=] 操作(或并发读取,取决于互斥类型)。
使用shared_ptr
基本上意味着没有数据的单一所有者。虽然这是可以管理的(例如引用计数),但它并不总是最好的解决方案(记住循环依赖、需要 weak_ptr
等)——有时最好找出资源的单一所有者,谁将负责在不再需要时释放它(例如,如果你有一个工作线程池,它可能是产生其他线程的线程) - 当然,你必须保证它比其他人活得更久,以使每个人都可以访问数据。所以你有几个选项来管理对象的生命周期:
- "borrow" 来自 boost/C++11 标准 library/Loki/some-other-existing-implementation 的代码(检查许可证以验证您是否可以使用它们)而不使用整个库 - 您可能需要对它们进行更改
- 写你自己的智能指针 - 很难而且只为专业人士 - 完全不推荐
- select 资源的单一所有者 - 这是我推荐的
谈到访问冲突管理,基本上有两种方法:
- 使用某种锁来管理它们(我假设你有一把锁)
- 通过只允许一个线程写入对象来避免它们。其他通常可能想要的将不得不请求 "owner thread" 的写入。这种方法非常适合单个资源所有者,但它更像是一个参与者模型,而不是典型的共享内存多线程,因此可能很难在遗留应用程序中引入。
您可以将锁与任何树内存管理方法一起使用,单作者方法最适合单所有者。请注意,这是范例的重大变化,可能需要大量工作来实现诸如消息队列和工作人员之类的东西。
如果您已经拥有基础设施(队列、工作人员等),我建议您考虑单一所有者、单一作者方法,否则带锁的单一所有者可能是一个很好的解决方案。如果您无法 select 单一所有者,请从现有库中提取代码 - 不要自己编写,因为您会犯一些错误,多线程环境中的内存管理确实 很难
编辑 1
既然你把问题说清楚了,我觉得回答有点太高了,我再补充一些细节。
你能做的最简单的事情是 return 一个 BigData&
而不是 BigData*
- 那时没有人应该删除它(当然这是可能的,但实际上一切都在 C++ 中) .否则,您还可以:
- 仅允许单个线程使用单个
BigData
实例 -(例如通过存储额外的std::map<int, thread_id>
以及有关已使用BigData
的信息 - 仅当您不需要并发访问时从多个线程到同一个实例 - return 类似
BigDataProxy
而不是BigData
-Proxy
应该有一个特殊的资源移除函数,然后由 [=77 执行=] - 这实际上只是shared_ptr
的特例,但实现起来可能更简单。
从概念上讲,Proxy
类似于(伪代码 - 忽略 private/public 成员等):
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;
}
请注意,这是伪代码,这里的异常处理会很困难。