diff options
author | Jacques Lucke <jacques@blender.org> | 2022-11-08 17:50:49 +0300 |
---|---|---|
committer | Jacques Lucke <jacques@blender.org> | 2022-11-08 17:50:49 +0300 |
commit | c73ae711bf403585de403bcfeb7519dd989d4c15 (patch) | |
tree | 06bf03e41b3e0476bf21e01a245f035ee227c720 /source/blender/blenlib | |
parent | 1d71f82033f1ec3ad51195cfa64c59fcf0cd6ccc (diff) |
BLI: new basic CacheMutex
This patch introduces a new `CacheMutex` which makes it easy to implement
lazily computed caches in e.g. `Curves`. For more details see `BLI_cache_mutex.hh`.
Differential Revision: https://developer.blender.org/D16419
Diffstat (limited to 'source/blender/blenlib')
-rw-r--r-- | source/blender/blenlib/BLI_cache_mutex.hh | 106 | ||||
-rw-r--r-- | source/blender/blenlib/CMakeLists.txt | 2 | ||||
-rw-r--r-- | source/blender/blenlib/intern/cache_mutex.cc | 25 |
3 files changed, 133 insertions, 0 deletions
diff --git a/source/blender/blenlib/BLI_cache_mutex.hh b/source/blender/blenlib/BLI_cache_mutex.hh new file mode 100644 index 00000000000..8e2a0d1b1a5 --- /dev/null +++ b/source/blender/blenlib/BLI_cache_mutex.hh @@ -0,0 +1,106 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#pragma once + +/** + * A #CacheMutex is used to protect a lazily computed cache from being computed more than once. + * Using #CacheMutex instead of a "raw mutex" to protect a cache has some benefits: + * - Avoid common pitfalls like forgetting to use task isolation or a double checked lock. + * - Cleaner and less redundant code because the same locking patterns don't have to be repeated + * everywhere. + * - One can benefit from potential future improvements to #CacheMutex of which there are a few + * mentioned below. + * + * The data protected by #CacheMutex is not part of #CacheMutex. Instead, the #CacheMutex and its + * protected data should generally be placed next to each other. + * + * Each #CacheMutex protects exactly one cache, so multiple cache mutexes have to be used when a + * class has multiple caches. That is contrary to a "custom" solution using `std::mutex` where one + * mutex could protect multiple caches at the cost of higher lock contention. + * + * To make sure the cache is up to date, call `CacheMutex::ensure` and pass in the function that + * computes the cache. + * + * To tell the #CacheMutex that the cache is invalidated and to be re-evaluated upon next access + * use `CacheMutex::tag_dirty`. + * + * This example shows how one could implement a lazily computed average vertex position in an + * imaginary `Mesh` data structure: + * + * \code{.cpp} + * class Mesh { + * private: + * mutable CacheMutex average_position_cache_mutex_; + * mutable float3 average_position_cache_; + * + * public: + * const float3 &average_position() const + * { + * average_position_cache_mutex_.ensure([&]() { + * average_position_cache_ = actually_compute_average_position(); + * }); + * return average_position_cache_; + * } + * + * void tag_positions_changed() + * { + * average_position_cache_mutex_.tag_dirty(); + * } + * }; + * \endcode + * + * Possible future improvements: + * - Avoid task isolation when we know that the cache computation does not use threading. + * - Try to use a smaller mutex. The mutex does not have to be fair for this use case. + * - Try to join the cache computation instead of blocking if another thread is computing the cache + * already. + */ + +#include <atomic> +#include <mutex> + +#include "BLI_function_ref.hh" + +namespace blender { + +class CacheMutex { + private: + std::mutex mutex_; + std::atomic<bool> cache_valid_ = false; + + public: + /** + * Make sure the cache exists and is up to date. This calls `compute_cache` once to update the + * cache (which is stored outside of this class) if it is dirty, otherwise it does nothing. + * + * This function is thread-safe under the assumption that the same parameters are passed from + * every thread. + */ + void ensure(FunctionRef<void()> compute_cache); + + /** + * Reset the cache. The next time #ensure is called, it will recompute that code. + */ + void tag_dirty() + { + cache_valid_.store(false); + } + + /** + * Return true if the cache currently does not exist or has been invalidated. + */ + bool is_dirty() const + { + return !this->is_cached(); + } + + /** + * Return true if the cache exists and is valid. + */ + bool is_cached() const + { + return cache_valid_.load(std::memory_order_relaxed); + } +}; + +} // namespace blender diff --git a/source/blender/blenlib/CMakeLists.txt b/source/blender/blenlib/CMakeLists.txt index 2ac77f000e9..693a4d98675 100644 --- a/source/blender/blenlib/CMakeLists.txt +++ b/source/blender/blenlib/CMakeLists.txt @@ -54,6 +54,7 @@ set(SRC intern/bitmap_draw_2d.c intern/boxpack_2d.c intern/buffer.c + intern/cache_mutex.cc intern/compute_context.cc intern/convexhull_2d.c intern/cpp_type.cc @@ -178,6 +179,7 @@ set(SRC BLI_bounds.hh BLI_boxpack_2d.h BLI_buffer.h + BLI_cache_mutex.hh BLI_color.hh BLI_color_mix.hh BLI_compiler_attrs.h diff --git a/source/blender/blenlib/intern/cache_mutex.cc b/source/blender/blenlib/intern/cache_mutex.cc new file mode 100644 index 00000000000..db474b1ef87 --- /dev/null +++ b/source/blender/blenlib/intern/cache_mutex.cc @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BLI_cache_mutex.hh" +#include "BLI_task.hh" + +namespace blender { + +void CacheMutex::ensure(const FunctionRef<void()> compute_cache) +{ + if (cache_valid_.load(std::memory_order_acquire)) { + return; + } + std::scoped_lock lock{mutex_}; + /* Double checked lock. */ + if (cache_valid_.load(std::memory_order_relaxed)) { + return; + } + /* Use task isolation because a mutex is locked and the cache computation might use + * multi-threading. */ + threading::isolate_task(compute_cache); + + cache_valid_.store(true, std::memory_order_release); +} + +} // namespace blender |