【问题标题】:How to create thread safe cache of shared_ptr如何创建 shared_ptr 的线程安全缓存
【发布时间】:2018-06-20 13:57:26
【问题描述】:

我有一个读取大量文件的代码。一些文件可以被缓存。消费者在请求文件时收到shared_ptr。如果文件仍在内存中,其他消费者可以请求此文件并从缓存中获取它。如果文件不在内存中,则将其加载并放入缓存中。

简化代码:

struct File
{
    File(std::string);
    bool AllowCache() const;
};

typedef std::shared_ptr<File> SharedPtr;
typedef std::weak_ptr<File> WeakPtr;
std::map<std::string, WeakPtr> Cache;

SharedPtr GetFile(std::wstring Name)
{
    auto Found = Cache.find(Name);
    if (Found != Cache.end())
        if (auto Exist = Found->second.lock())
            return Exist;

    auto New = boost::make_shared<File>(Name);
    if (New->AllowCache())
        Cache[Name] = New;
    return New;
}

我的问题是:如何使这段代码安全?即使我通过互斥锁保护GetFile() 的内容,它仍然可以从weak_ptr::lock() 返回非空指针,而其他线程正在运行指向File 对象的析构函数。

我看到了一些解决方案,例如:

  1. shared_ptrs 存储在缓存中并运行一个单独的线程,该线程将 不断删除shared_ptr-s 和use_count()==1(我们称之为Cleanup())。
  2. shared_ptrs 存储在缓存中,并要求消费者使用shared_ptr&lt;File&gt; 的特殊包装器。这个包装器将有shared_ptr&lt;File&gt; 作为成员,并将reset() 它在析构函数中,然后调用Cleanup()

第一种解决方案有点矫枉过正。第二种解决方案需要重构我项目中的所有代码。这两种解决方案都对我不利。有没有其他方法可以使它安全?

【问题讨论】:

  • 作为文体旁注:请重新考虑对变量和函数名称使用大写字母。它使变量和函数乍一看像类型/声明,这使得阅读代码变得更加困难(至少对我而言)。在 C++ 中命名 anything New 相当...粗体(我花了 30 年代思考“新的?新的什么?你错过了指针分配的类型吗?”在我意识到 New 之前一个变量...)
  • 您为什么不直接使用文件的共享内存映射而不自己担心所有这些?
  • 正如您所指出的,您可以使用简单的互斥锁使您的缓存线程安全。真正的问题是如何使访问从缓存线程获得的共享对象安全?这是一个独立于缓存本身的问题。
  • "即使我通过互斥锁保护 GetFile() 的内容,它仍然可以从 weak_ptr::lock() 返回非空指针,而其他线程正在运行指向 File 对象的析构函数." - 你确定吗?我认为std::shared_ptr 会在破坏其目标之前阻止std::weak_ptr 访问。
  • @CharonX 这对我来说一开始看起来像虚幻引擎风格,他们有大写变量命名......但在通常的 C++ 代码中这有点令人困惑

标签: c++ multithreading shared-ptr


【解决方案1】:

除非我误解了您所描述的场景,否则如果另一个线程正在运行 File 对象的析构函数,则 WeakPtrlock() 将失败(即返回虚拟 shared_ptr)。这就是共享指针和弱指针的逻辑。所以 - 您当前的解决方案在这方面应该是线程安全的;但是 - 当您添加或删除 elemnrs 时,它可能是非线程安全的。例如,在这个问题中阅读相关内容:C++ Thread-Safe Map

【讨论】:

  • 我写了一个测试(见下文)。它证明你是对的。 weak_ptr 不返回指向死对象的指针。
【解决方案2】:

我预计以下代码会失败。但事实并非如此。似乎weak_ptr::lock() 不会返回指向处于销毁过程中的对象的指针。如果是这样,这是一个最简单的解决方案,只需添加一个互斥锁,不用担心weak_ptr::lock() 返回死对象。

char const *TestPath = "file.xml";

void Log(char const *Message, std::string const &Name, void const *File)
{
    std::cout << Message
        << ": " << Name
        << " at memory=" << File
        << ", thread=" << std::this_thread::get_id()
        << std::endl;
}

void Sleep(int Seconds)
{
    std::this_thread::sleep_for(std::chrono::seconds(Seconds));
}

struct File
{
    File(std::string Name) : Name(Name)
    {
        Log("created", Name, this);
    }

    ~File()
    {
        Log("destroying", Name, this);
        Sleep(5);
        Log("destroyed", Name, this);
    }

    std::string Name;
};

std::map<std::string, std::weak_ptr<File>> Cache;
std::mutex Mutex;

std::shared_ptr<File> GetFile(std::string Name)
{
    std::unique_lock<std::mutex> Lock(Mutex); // locking is added
    auto Found = Cache.find(Name);
    if (Found != Cache.end())
        if (auto Exist = Found->second.lock())
        {
            Log("found in cache", Name, Exist.get());
            return Exist;
        }

    auto New = std::make_shared<File>(Name);
    Cache[Name] = New;
    return New;
}

void Thread2()
{
    auto File = GetFile(TestPath);
    //Sleep(3); // uncomment to share file with main thead
}

int main()
{
    std::thread thread(&Thread2);
    Sleep(1);
    auto File = GetFile(TestPath);
    thread.join();
    return 0;
}

我的期望:

thread2: created
thread2: destroying
thread1: found in cache <--- fail. dead object :(
thread2: destroyed

VS2017 结果:

thread2: created
thread2: destroying
thread1: created <--- old object is not re-used! great ;)
thread2: destroyed

【讨论】:

    猜你喜欢
    • 2011-09-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-07
    • 1970-01-01
    • 2019-06-07
    相关资源
    最近更新 更多