Lock for multi-threaded hot swap

In my multi-threaded graphics application, I have certain assets like images, models, sound files, etc. Some of them are loaded from files on disk. When these files change, I want to automatically reload them and update the corresponding assets that can be used throughout the application. A similar use case is LOD. When models go far away from the camera, I want to replace them with cheaper, less detailed versions.

When replacing assets, other parts of the application run on different threads and can read those assets. So I need a lock. How can I enforce proper asset swap locking, making it as easy as possible for other parts of the application to use?

For example, I could provide an abstract base class for assets. This can contain a shared mutex. Then there will be a loader class that stores assets internally and returns references to them.

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;

      

However, there are potentially many assets, so let's try fewer mutexes. For example, the bootloader can keep a list of locked assets. There will only be one mutex to access the list. Moreover, we no longer need a base asset 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;

      

However, I still feel that this is not the best solution. If there are thousands of assets, they are often handled in bulk. Thus, there is a loop over the asset vector, and a locking mechanism will be required at each iteration. To lock, I also need to remember the filenames of all the assets I want to use. (Also, it's strange that the bootloader holds locks.)

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);        
}

      

Is there a better way to do this? I feel like I am overcomplicating this and watching a much simpler solution. How is this situation usually resolved? My goals are to have a simple interface and good performance for iterating over large collections of assets.

+3


source to share


1 answer


Using mutexes almost guarantees that you will have stuttering in your use of assets. Please note that you are starting to download a different version of the asset, then the mapper wants to use it, but it is blocked, and therefore the thread is blocked until it is unblocked.

You can use share_ptr instead, the consumer will keep the share_ptr in the asset until it is no longer in use.

  • don't forget to save it if you have specified a pointer to the data in the object to the render function, otherwise the render function may refer to NULL).
  • Don't forget to provide pointers after the render no longer uses it.

The bootloader just loads the new data, and when the download completes, an atomic switch occurs on the assets, so the next request from the consumer gets a new asset.

  • If 2 consumers get 2 different versions of the same asset, is this really a huge problem? It should be resolved after one update, if only its music or sounds or others with duration.


ps. Some code checking.

  • When using a mutex, try using lock_guard as a RAII pattern.
  • It is often best to avoid std :: map (and std :: list) and use std :: unordered_map or std :: vector instead.
  • hard to read type declarations

    std :: Map <std :: Pair <std :: string, std :: type_index>, void *>

if you are using using you can write something like this

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

std::map<AssertId, void*>

      

if that's what you really mean.

+1


source







All Articles