【问题标题】:Lazy initialized caching... how do I make it thread-safe?延迟初始化缓存...如何使其成为线程安全的?
【发布时间】:2011-11-11 17:06:20
【问题描述】:

这就是我所拥有的:

  • Windows 服务
    • C#
    • 多线程
    • 服务使用读写锁(一次多次读取,写入阻塞其他读/写线程)
  • 一个简单的自写数据库
    • C++
    • 小到可以放入内存
    • 足够大,不想在启动时加载它(例如 10GB)
    • 读取性能非常重要
    • 写作没那么重要
    • 树形结构
    • 保存在树节点中的信息存储在文件中
    • 为提高性能,文件仅在首次使用和缓存时加载
    • 延迟初始化以加快数据库启动

由于数据库会非常频繁地访问这些节点信息(每秒数千次),而且我不经常写,我想使用某种双重检查锁定模式。

我知道这里有很多关于双重检查锁定模式的问题,但似乎有很多不同的意见,所以我不知道什么是最适合我的情况。你会如何处理我的设置?

这是一个例子:

  • 一棵有 100 万个节点的树
  • 每个节点存储一个键值对列表(存储在一个文件中用于持久化,文件大小大小:10kB)
  • 第一次访问节点时,列表被加载并存储在一个映射中(sth. like std::map)
  • 下次访问这个节点的时候就不用再加载文件了,直接从map中获取就行了。
  • 唯一的问题:两个线程第一次同时访问节点,想要 写入缓存映射。这不太可能发生,但也不是不可能。这就是我需要线程安全的地方,这不应该花费太多时间,因为我通常不需要它(尤其是当整个数据库都在内存中时)。

【问题讨论】:

  • 你已经拥有这一切了吗?然后我就坐下来,运送它并享受意外之财。
  • 你能比“几个 GB”更精确地估计大小吗?我认为非常很难将整个数据库放入内存中。例如,您可能会考虑存储压缩的数据(例如,一些基于 LZ 的压缩)以提供帮助。即使保存 几个 磁盘访问也可以涵盖相当多的解压缩时间。
  • @Kerrek:我想坐下来享受一下,但现在我不能以多线程方式使用数据库,因为它还不是完全线程安全的。因此这个线程;)
  • @Jerry Coffin:目的是将整个数据库保存在内存中。目前,数据库足够小,可以放入一台计算机的 RAM。 DB变大需要的时间,我们希望有一个分布式的解决方案。数据库信息仅出于持久性原因保存在文件中。我只是不想在将所有内容加载到内存时等待数据库启动,所以我使用的是惰性缓存,这使得多线程很难使用它。

标签: c++ windows multithreading caching double-checked-locking


【解决方案1】:

关于双重检查锁定:

class Foo
{
  Resource * resource;

  Foo() : resource(nullptr) { }
public:
  Resource & GetResource()
  {
    if(resource == nullptr)
    {
      scoped_lock lock(mutex); 
      if(resource == nullptr)
        resource = new Resource();
    }
    return *resource;
  }
}

检查资源地址是否为空时,它不是线程安全的。因为资源指针有可能在初始化指向它的 Resource 对象之前被分配给一个非空值。

但是使用 C++11 的“原子”特性,您可能拥有双重检查锁定机制。

class Foo
{
  Resource * resource;
  std::atomic<bool> isResourceNull;
public:
  Foo() : resource(nullptr), isResourceNull(true) { }

  Resource & GetResource()
  {
    if(isResourceNull.load())
    {
      scoped_lock lock(mutex); 
      if(isResourceNull.load())
      {
        resource = new Resoruce();
        isResourceNull.store(false);
      }
    }
    return *resource;
  }
}

编辑:没有原子

#include <winnt.h>

class Foo
{
  volatile Resource * resource;

  Foo() : resource(nullptr) { }
public:
  Resource & GetResource()
  {
    if(resource == nullptr)
    {
      scoped_lock lock(mutex); 
      if(resource == nullptr)
      {
        Resource * dummy = new Resource();
        MemoryBarrier(); // To keep the code order
        resource = dummy;  // pointer assignment
      }
    }
    return  *const_cast<Resource*>(resource);
  }
}

MemoryBarrier() 确保首先创建dummy,然后分配给resource。 根据this link,指针分配在 x86 和 x64 系统中将是原子的。而volatile保证resource的值不会被缓存。

【讨论】:

  • 是的,我知道 Meyers 和 Alexandrescu 指出的问题。但是必须有某种方法来实现它——在他们的论文中,他们谈到了一个内存屏障,它依赖于平台/编译器。我正在使用 MSVC 2010,那么我可以使用哪种内存屏障? C++11 还不是一个选项...
  • @Ben:还有 tbb::atomic 可以使用。但它的免费版本是 GPL 许可的。
  • MemoryBarrier() 看起来不错,我要试试!谢谢!
【解决方案2】:

您是在问如何使读取 DB 或读取 Nodes 线程安全吗?

如果您尝试使用后者并且您不经常写作,那么为什么不将您的节点设为immutable,句号呢?如果你需要写一些东西,然后从现有节点复制数据,修改它并创建另一个节点,然后你可以将其放入数据库中。

【讨论】:

  • 我猜我的帖子不够清楚。我没有使用像 MySQL 或 Oracle 这样的数据库......我正在构建的树结构是数据库。当然是一个非常简单的。查询数据库会导致访问许多节点,从中检索信息,合并信息并返回结果。我将编辑我的问题以提供更多信息。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-11-17
  • 2015-07-27
  • 2014-07-11
  • 1970-01-01
  • 2012-03-16
  • 2020-02-09
  • 1970-01-01
相关资源
最近更新 更多