C++ 多线程热插拔的锁定

C++ 多线程热插拔的锁定,c++,multithreading,design-patterns,locking,C++,Multithreading,Design Patterns,Locking,在我的多线程图形应用程序中,我拥有某些资产,如图像、模型、声音文件等。其中一些是从磁盘上的文件加载的。当这些文件发生更改时,我希望自动重新加载它们并更新可能在整个应用程序中使用的相应资产。类似的用例是LOD。当模型远离相机时,我想用更便宜、更不详细的版本来代替它们 当资产被替换时,应用程序的其他部分在不同的线程中运行,并可能读取这些资产。所以我需要一些锁。如何提供适当的锁定来替换资产,同时使应用程序的其他部分尽可能容易地使用它们 例如,我可以为资产提供一个抽象基类。这可能包含一个共享互斥体。然后

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

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

例如,我可以为资产提供一个抽象基类。这可能包含一个共享互斥体。然后,将有一个装入器类,它在内部存储资产并返回对它们的引用

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;
类别资产{
公众:
std::共享互斥访问;
};
类加载器{
公众:
模板
加载(&L)(标准::字符串文件名);
私人:
地图资产;
};
模板
类AssetLoaderTraits;
但是,可能有很多资产,所以让我们尝试更少的互斥。例如,加载程序可以保存一个锁定资产的列表。只有一个互斥体可以访问列表。此外,我们不再需要资产基本类

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;
类加载器{
公众:
模板
加载(&L)(标准::字符串文件名);
无效锁(std::字符串文件名);
bool try_lock(std::string filename,std::chronos::duration trying);
无效解锁(标准::字符串文件名);
私人:
地图资产;
std::共享互斥映射访问;
std::映射锁定的资源;
};
模板
类AssetLoaderTraits;
然而,我仍然觉得这不是最好的解决方案。如果有成千上万的资产,它们通常是批量处理的。因此,在资产向量上有一个循环,在每次迭代中都需要锁定机制。对于锁定,我还需要记住我要使用的所有资产的文件名。(此外,装载机持有锁的感觉很奇怪。)

std::矢量图像;
Image&Image=loader.load(“/path/to/Image.png”);
images.push_back(std::make_pair(“/path/to/image.png”,image));
// ...
用于(自动和输入:图像){
std::string&filename=i.first;
Image&Image=i.second;
loader.lock(文件名);
// ...
loader.unlock(文件名);
}

有更好的方法吗?我觉得我已经把这件事复杂化了,并且已经找到了一个更简单的解决方案。这种情况通常如何解决?我的目标是拥有一个简单的界面和良好的性能,以便在大型资产集合上进行迭代。

使用互斥体几乎可以保证在使用资产时会出现口吃。考虑到您开始加载另一个版本的资产,然后显示例程想要使用它,但是它被锁定,因此线程在被解锁之前被阻塞。 您可以使用share_ptr,消费者将保留资产上的share_ptr,直到不再使用为止

  • 如果将对象中的数据指针指定给渲染函数,请记住将其保留,否则渲染函数可能引用空值)
  • 记住在渲染不再使用指针后取消引用指针
加载程序只加载新数据,加载完成后,会对资源进行原子切换,以便消费者的下一个请求获得新资源

  • 如果两个消费者获得相同资产的两个不同版本,那么这是一个巨大的问题吗?它应该在一次更新后解决,除非它的音乐或声音或其他持续时间
附言。 一些代码审查

  • 使用互斥时,请尝试使用as RAII模式
  • 由于性能原因,通常最好避免使用std::map(和std::list),而使用std::unordered_map或std::vector
  • 难以阅读的类型声明

    std::map,void*>

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

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

std::map<AssertId, void*>
使用AssetId=std::pair;
标准::地图

如果这是你真正的意思。

你基本上建议向消费者分发双指针吗?然后,加载器可以更改指针的目标,这应该是一个原子操作。两个消费者可以使用不同的版本。然而,声音示例让我思考,因为我希望播放的声音在重新加载时停止,然后开始新的声音(可能在相同的位置恢复)。为此,我最好还是回电话。感谢您的代码审查!std::shared_ptr是一个引用计数器,资产应该实现一个负责清理的析构函数。嗯,哪个原子操作会允许交换共享指针指向的资产?使用者将拥有自己的共享_ptr,您只需用新的值替换表中的值,然后,当消费者下次要求价值时,他们会得到新的。我很难理解这一点。对于较大的对象,替换操作如何是原子的?有了双间接寻址,我就有机会了,因为在现代CPU上,只需要更改一个原子指针地址(但我必须检查一下)。
using AssetId = std::pair<std::string, std::type_index>;

std::map<AssertId, void*>