如何创建 shared_ptr 的线程安全缓存

How to create thread safe cache of shared_ptr

我有一个读取大量文件的代码。可以缓存一些文件。消费者在请求文件时收到 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() 的内容,它仍然可以 return 来自 weak_ptr::lock() 的非空指针,而其他线程是 运行 指向 [=15= 的析构函数] 对象。

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

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

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

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

我预计以下代码会失败。但事实并非如此。似乎 weak_ptr::lock() 不会 return 指向正在销毁过程中的对象的指针。如果是这样,这是一个最简单的解决方案,只需添加一个互斥锁,而不必担心 returning 死对象 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