锁定多线程热插拔

Locking for multi-threaded hot swapping

在我的多线程图形应用程序中,我有某些资产,如图像、模型、声音文件等。其中一些是从磁盘上的文件加载的。当这些文件发生变化时,我想自动重新加载它们并更新可能在整个应用程序中使用的相应资产。一个类似的用例是 LOD。当模型离相机很远时,我想用更便宜、细节更少的版本替换它们。

当资产被替换时,应用程序的其他部分 运行 在不同的线程中并且可以读取这些资产。所以我需要一些锁定。我如何提供适当的锁定来替换资产,同时让应用程序的其他部分尽可能容易地使用它们?

例如,我可以为资产提供一个抽象基础 class。这可能包含一个共享的互斥体。然后,将有一个加载器 class,它在内部存储资产和对它们的 returns 引用。

class Asset {
public:
    std::shared_mutex access_;
};

class Loader {
public:
    template <typename T>
    T &load(std::string filename);
private:
    std::map<std::pair<std::string, std::type_index>, void*> assets_;
};

template <typename T>
class AssetLoaderTraits;

但是,可能有很多资产,所以让我们尝试更少的互斥锁。例如,加载器可以保留一个锁定资产列表。将只有一个互斥锁来访问列表。此外,我们不再需要资产基础 class。

class Loader {
public:
    template <typename T>
    T &load(std::string filename);
    void lock(std::string filename);
    bool try_lock(std::string filename, std::chronos::duration trying);
    void unlock(std::string filename);
private:
    std::map<std::pair<std::string, std::type_index>, void*> assets_;
    std::shared_mutex map_access_;
    std::map<std::string> locked_assets_;
};

template <typename T>
class AssetLoaderTraits;

但是,我仍然觉得这不是最好的解决方案。如果有数千个资产,它们通常会被批量处理。所以在资产向量上有一个循环,并且在每次迭代中都需要锁定机制。为了锁定,我还需要记住我要使用的所有资产的文件名。 (还有,loader持有锁感觉怪怪的。)

std::vector<std::pair<std::string, Image&>> images;
Image &image = loader.load<Image>("/path/to/image.png");
images.push_back(std::make_pair("/path/to/image.png", image));
// ...

for (auto &i : images) {
    std::string &filename = i.first;
    Image &image = i.second;
    loader.lock(filename);
    // ...
    loader.unlock(filename);        
}

有更好的方法吗?我觉得我过于复杂了,并且监督了一个更简单的解决方案。这种情况一般是怎么解决的?我的目标是为大型资产集合的迭代提供一个简单的界面和良好的性能。

使用互斥体几乎可以保证您在使用资产时会卡顿。假设您开始加载另一个版本的资产,然后显示例程想要使用它,但它被锁定并且线程因此被阻塞,直到它被解锁。

您可以改用 share_ptr,消费者将在资产上保留 share_ptr,直到不再使用为止。

  • 如果您将指向对象中数据的指针提供给渲染函数,请记住保留它,否则渲染函数可能会引用 NULL)。
  • 记得在渲染不再使用指针后取消引用它。

加载器只加载新数据,加载完成后对资产进行原子切换,以便消费者的下一个请求获得新资产。

  • 如果 2 个消费者得到同一个资产的 2 个不同版本,那么这是一个大问题吗?它应该在一次更新后解决,除非它的音乐或声音或其他持续时间。

ps。 一些代码审查。

  • 使用互斥量时,请尝试使用 lock_guard 作为 RAII 模式。
  • 通常最好避免使用 std::map(和 std::list)并使用 std::unordered_map 或 std::vector,因为性能。
  • 难以阅读类型声明

    std::map< std::pair< std::string, std::type_index>, void*>

如果你使用 using 你可以写这样的东西

using AssetId = std::pair<std::string, std::type_index>;

std::map<AssertId, void*>

如果那是你真正的意思。