Loading...
Searching...
No Matches
concurrentMap.h
1//
2// Copyright 2023 Pixar
3//
4// Licensed under the terms set forth in the LICENSE.txt file available at
5// https://openusd.org/license.
6//
7#ifndef EXT_RMANPKG_PLUGIN_RENDERMAN_PLUGIN_HD_PRMAN_CONCURRENT_MAP_H
8#define EXT_RMANPKG_PLUGIN_RENDERMAN_PLUGIN_HD_PRMAN_CONCURRENT_MAP_H
9
10#include "pxr/pxr.h"
11
12#include <tbb/concurrent_unordered_map.h>
13#include <tbb/spin_rw_mutex.h>
14
15#include <atomic>
16#include <cstddef>
17#include <functional>
18#include <tuple>
19#include <type_traits>
20#include <utility>
21
22PXR_NAMESPACE_OPEN_SCOPE
23
24// A simple concurrent hashmap built from tbb::concurrent_unordered_map but
25// with a simpler interface. Thread-safe operations (insertion, retrieval,
26// const iteration) happen under a shared_lock, while unsafe operations
27// (erase, clear, non-const iteration) use an exclusive lock. This way, the
28// thread-safe operations can all run concurrently with one another, relying
29// on tbb::concurrent_unordered_map's thread safety, but will never run
30// while an unsafe operation is in progress, nor will the unsafe operations
31// start while a safe one is running.
32//
33// Currently supports two value types (T):
34// - Any T (other than std::atomic<U>) that is move- or default-
35// constructible: provides get() and set().
36// - std::atomic<U> were U is default-constructible: provides get() & load().
37// When U is an integer type, additionally provides inc() & dec().
38// Neither movable nor copyable.
39template<
40 typename Key,
41 typename T,
42 typename Hash = std::hash<Key>,
43 typename KeyEqual = std::equal_to<Key>>
44class HdPrmanConcurrentMap
45{
46private:
47 template<typename U>
48 struct _is_atomic : std::false_type { };
49
50 template<typename U>
51 struct _is_atomic<std::atomic<U>> : std::true_type { };
52
53 // XXX: std::atomic<U>::value_type is not available until C++20; these
54 // fill the gap until then.
55 template<typename U>
56 struct _atomic_value_type; // undefined for non-atomic U
57
58 template<typename U>
59 struct _atomic_value_type<std::atomic<U>> { using type = U; };
60
61 template<typename U>
62 using _atomic_value_t = typename _atomic_value_type<U>::type;
63
64 static_assert(
65 _is_atomic<T>::value ||
66 std::is_move_constructible_v<T> ||
67 std::is_default_constructible_v<T>,
68 "HdPrmanConcurrentMap<Key, T>: T must be move- or default- "
69 "constructible or std::atomic<U> for integral U.");
70
71public:
72 T& get(const Key& key)
73 {
74 if constexpr (_is_atomic<T>::value) {
75 tbb::spin_rw_mutex::scoped_lock lock(_mutex, false);
76 return _get_atomic(key, lock);
77 }
78 if constexpr (std::is_move_constructible_v<T>) {
79 tbb::spin_rw_mutex::scoped_lock lock(_mutex, false);
80 return _map[key];
81 }
82 tbb::spin_rw_mutex::scoped_lock lock(_mutex, false);
83 auto it = _map.find(key);
84 if (it == _map.end()) {
85 if (!lock.upgrade_to_writer()) {
86 it = _map.find(key);
87 }
88 if (it == _map.end()) {
89 it = _map.emplace(
90 std::piecewise_construct,
91 std::forward_as_tuple(key),
92 std::tuple<>{}).first;
93 }
94 }
95 return it->second;
96
97 }
98
99 // Executes the given function under shared lock if the map has a
100 // value for key. WARNING: The function must not call any methods on the
101 // map itself; doing so will cause a deadlock!
102 bool withIfPresent(const Key& key, std::function<void(T&)> fn)
103 {
104 tbb::spin_rw_mutex::scoped_lock lock(_mutex, false);
105 auto it = _map.find(key);
106 if (it == _map.end()) { return false; }
107 fn(it->second);
108 return true;
109 }
110
111 // Iterate the map with a non-const value reference under exclusive lock.
112 // WARNING: The function must not call any methods on the map itself;
113 // doing so will cause a deadlock!
114 void iterate(std::function<void(const Key&, T&)> fn)
115 {
116 // exclusive lock
117 tbb::spin_rw_mutex::scoped_lock lock(_mutex, true);
118 for (auto& p : _map) {
119 fn(p.first, p.second);
120 }
121 }
122
123 // Iterate the map with a const value reference under shared lock.
124 // WARNING: The function must not call any methods on the map itself;
125 // doing so will cause a deadlock!
126 void citerate(std::function<void(const Key&, const T&)> fn) const
127 {
128 tbb::spin_rw_mutex::scoped_lock lock(_mutex, false);
129 for (const auto& p : _map) {
130 fn(p.first, p.second);
131 }
132 }
133
134 // Gives the count of keys currently in the map
135 size_t size() const
136 {
137 tbb::spin_rw_mutex::scoped_lock lock(_mutex, false);
138 return _map.size();
139 }
140
141 // Erase the given key from the map under exclusive lock
142 void erase(const Key& key)
143 {
144 // tbb::concurrent_hash_map::erase() is not thread-safe
145 tbb::spin_rw_mutex::scoped_lock lock(_mutex, true);
146 _map.unsafe_erase(key);
147 }
148
149 // Clear all map entries under exclusive lock
150 void clear()
151 {
152 // tbb::concurrent_hash_map::clear() is not thread-safe
153 tbb::spin_rw_mutex::scoped_lock lock(_mutex, true);
154 _map.clear();
155 }
156
157 // Set value for key. Available only for copy-assignable non-atomic T
158 template<
159 typename U = T,
160 typename = std::enable_if_t<
161 !_is_atomic<U>::value &&
162 std::is_copy_assignable_v<U>>>
163 void set(const Key& key, const T& val)
164 {
165 tbb::spin_rw_mutex::scoped_lock lock(_mutex, false);
166 if constexpr (std::is_move_constructible_v<T>) {
167 _map[key] = val;
168 return;
169 }
170 auto it = _map.find(key);
171 if (it == _map.end()) {
172 if (!lock.upgrade_to_writer()) {
173 it = _map.find(key);
174 }
175 if (it == _map.end()) {
176 if constexpr (std::is_copy_constructible_v<T>) {
177 _map.insert({ key, val });
178 return;
179 }
180 it = _map.emplace(
181 std::piecewise_construct,
182 std::forward_as_tuple(key),
183 std::tuple<>{}).first;
184 }
185 }
186 it->second = val;
187 }
188
189 // Load the atomic value for key. Only available for atomic T.
190 template<
191 typename U = T,
192 typename = std::enable_if_t<_is_atomic<U>::value>>
193 _atomic_value_t<U> load(
194 const Key& key,
195 std::memory_order order = std::memory_order_seq_cst)
196 {
197 tbb::spin_rw_mutex::scoped_lock lock(_mutex, false);
198 return _get_atomic(key, lock).load(order);
199 }
200
201 // Increment the atomic value for key. Returns value after increment.
202 // Only available for atomic integral T.
203 template<
204 typename U = T,
205 typename = std::enable_if_t<
206 _is_atomic<U>::value &&
207 std::is_integral_v<_atomic_value_t<U>>>>
208 _atomic_value_t<U> inc(const Key& key)
209 {
210 tbb::spin_rw_mutex::scoped_lock lock(_mutex, false);
211 return ++_get_atomic(key, lock);
212 }
213
214 // Decrement the atomic value for key. Returns value after decrement.
215 // Only available for atomic integral T.
216 template<
217 typename U = T,
218 typename = std::enable_if_t<
219 _is_atomic<U>::value &&
220 std::is_integral_v<_atomic_value_t<U>>>>
221 _atomic_value_t<U> dec(const Key& key)
222 {
223 tbb::spin_rw_mutex::scoped_lock lock(_mutex, false);
224 return --_get_atomic(key, lock);
225 }
226
227private:
228 template<typename U = T, typename = std::enable_if_t<_is_atomic<U>::value>>
229 T& _get_atomic(const Key& key, tbb::spin_rw_mutex::scoped_lock& lock)
230 {
231 auto it = _map.find(key);
232 if (it == _map.end()) {
233 if (!lock.upgrade_to_writer()) {
234 it = _map.find(key);
235 }
236 if (it == _map.end()) {
237 // Prior to C++20, std::atomic<U>'s default constructor does not
238 // initialize the value held by the atomic. We must default
239 // construct the value first and pass it as an argument to
240 // emplace(), which will then invoke std::atomic<U>(U desired)
241 // instead, which does initialize the value. Under C++20, the
242 // third argument to emplace() may be simplified to
243 // std::tuple<>{}.
244 it = _map.emplace(
245 std::piecewise_construct,
246 std::forward_as_tuple(key),
247 std::forward_as_tuple(_atomic_value_t<T>{})).first;
248 }
249 }
250 return it->second;
251 }
252
253 using _MapType = tbb::concurrent_unordered_map<Key, T, Hash, KeyEqual>;
254 _MapType _map;
255
256 mutable tbb::spin_rw_mutex _mutex;
257};
258
259PXR_NAMESPACE_CLOSE_SCOPE
260
261#endif // EXT_RMANPKG_PLUGIN_RENDERMAN_PLUGIN_HD_PRMAN_CONCURRENT_MAP_H
STL namespace.