保护数据设计模式



在我们的应用程序中,我们处理在工作线程中处理并在显示线程中访问的数据,并且我们有一个互斥锁来处理临界区。没什么特别的。

现在我们考虑重新编写代码,当前的锁定是由持有和处理数据的一方显式地完成的。我们想到了一个保存数据的单一实体,并且只以一种受保护的方式访问数据。

对于这个,我们有一个叫做GuardedData的类。调用者可以请求这样的对象,并且应该只在局部作用域中保留很短的时间。只要对象存在,它就保持锁。一旦对象被销毁,锁就会被释放。数据访问与锁定机制耦合在一起,而调用方没有任何显式的额外工作。类的名称提醒调用者当前的守卫。
template<typename T, typename Lockable>
class GuardedData {
    GuardedData(T &d, Lockable &m) : data(d), guard(m) {}
    boost::lock_guard<Lockable> guard;
    T &data;
    T &operator->() { return data; }
};

这又是一个非常简单的概念。操作符->模仿STL迭代器访问负载的语义。

现在我想知道:

  • 这种方法是众所周知的吗?
  • 是否有像这样的模板化类已经可用,例如在boost库中?

我问是因为我认为这是一个相当通用和可用的概念。

根据使用方式的不同,您几乎肯定会在某些时候出现死锁。如果你想操作两个数据块,那么你最终会锁定互斥锁两次并死锁(除非每个数据块都有自己的互斥锁——如果锁顺序不一致,也会导致死锁——在这种方案中,你无法控制这一点,否则会使它变得非常复杂)。除非你使用递归互斥锁,而这可能不是你想要的。

另外,你的GuardedData对象是如何传递的?Boost::lock_guard是不可复制的——它会引发互斥锁的所有权问题,即&

当读写线程需要数据时,将需要的部分数据复制到读写线程中可能更容易,这样可以保持临界区短。同样,写入器也会一次性提交数据模型。

实际上,查看器线程在给定时间获得所需数据的快照。这甚至可能完全适合位于运行线程的核心附近的cpu缓存,而不会使其进入RAM。当读取器处理底层数据时,写入线程可能会修改底层数据(但这会使视图无效)。然而,由于查看器有一个副本,它可以继续并提供与数据同步时的数据视图。

另一个选择是给视图一个指向数据的智能指针(它应该被视为不可变的)。如果编写器希望修改数据,则在此时复制数据,修改副本,并在完成时将指针切换到模型中的数据。这将需要在处理过程中阻塞所有的读/写程序,除非只有一个写程序。当阅读器下次请求数据时,它将获得新的副本。

大家都知道,我不确定。然而,我在Qt中使用了一个类似的机制,通常称为QMutexLocker。不同之处在于,您将数据与互斥锁绑定在一起。与你所描述的非常相似的机制是c#中线程同步的规范。

您的方法对于每次保护一个数据项很好,但如果需要保护更多数据项,则会变得麻烦。此外,看起来您的设计不会阻止我在共享的地方创建这个对象,并尽可能频繁地访问数据,认为它被保护得很好,但实际上不处理递归访问场景,也不处理多线程访问场景,如果它们发生在同一作用域中。

这个想法似乎有点不一致。它的使用告诉我,访问数据总是线程安全的,因为数据是受保护的。通常,这不足以确保线程安全。对受保护数据的操作顺序通常很重要,因此锁定实际上是面向范围的,而不是面向数据的。您可以在模型中通过保护一个虚拟对象并将保护对象包装在临时作用域中来解决这个问题,但是为什么不直接使用现有的互斥锁实现呢?

实际上,这是一个不错的方法,但是您需要确保它的预期用途被理解。

最新更新