HDK
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
unordered_map_concurrent.h
Go to the documentation of this file.
1 // Copyright 2008-present Contributors to the OpenImageIO project.
2 // SPDX-License-Identifier: BSD-3-Clause
3 // https://github.com/OpenImageIO/oiio
4 
5 
6 #pragma once
7 
8 #include <OpenImageIO/dassert.h>
9 #include <OpenImageIO/hash.h>
10 #include <OpenImageIO/thread.h>
11 
13 
14 
15 namespace pvt {
16 
17 // SFINAE test for whether class T has method `iterator find(key,hash)`.
18 // As described here: https://www.bfilipek.com/2016/02/sfinae-followup.html
19 // clang-format off
20 template <typename T>
22  using key_type = typename T::key_type;
23  using iterator_type = typename T::iterator;
24  template <typename U>
25  static constexpr std::false_type test(...) { return {}; }
26  template <typename U>
27  static constexpr auto test(U* u) ->
28  typename std::is_same<iterator_type, decltype(u->find(key_type(), size_t(0)))>::type { return {}; }
29 public:
30  static constexpr bool value = test<T>(nullptr);
31 };
32 // clang-format on
33 
34 } // namespace pvt
35 
36 
37 // Helper function: find_with_hash.
38 //
39 // Calls `map.find(key, hash)` if a method with that signature exists for
40 // the Map type, otherwise just calls `map.find(key)`.
41 //
42 // This lets us use unordered_map_concurrent with underlying bin map types
43 // that do (e.g., robin_map) or do not (e.g., std::unordered_map) support a
44 // find method taking a precomputed hash.
45 template<class Map, class Key,
47 typename Map::iterator
48 find_with_hash(Map& map, const Key& key, size_t hash)
49 {
50  return map.find(key, hash);
51 }
52 
53 template<class Map, class Key,
55 typename Map::iterator
56 find_with_hash(Map& map, const Key& key, size_t /*hash*/)
57 {
58  return map.find(key);
59 }
60 
61 
62 
63 /// unordered_map_concurrent provides an unordered_map replacement that
64 /// is optimized for concurrent access. Its principle of operation is
65 /// similar to Java's ConcurrentHashMap.
66 ///
67 /// With naive use of an unordered_map, multiple threads would have to
68 /// lock a mutex of some kind to control access to the map, either with
69 /// a unique full lock, or with a reader/writer lock. But as the number
70 /// of threads contending for this shared resource rises, they end up
71 /// locking each other out and the map becomes a thread bottleneck.
72 ///
73 /// unordered_map_concurrent solves this problem by internally splitting
74 /// the hash map into several disjoint bins, each of which is a standard
75 /// unordered_map. For any given map item, the hash of its key
76 /// determines both the bin as well as its hashing within the bin (i.e.,
77 /// we break a big hash map into lots of little hash maps,
78 /// deterministically determined by the key). Thus, we should expect
79 /// map entries to be spread more or less evenly among the bins. There
80 /// is no mutex that locks the map as a whole; instead, each bin is
81 /// locked individually. If the number of bins is larger than the
82 /// typical number of threads, most of the time two (or more) threads
83 /// accessing the map simultaneously will not be accessing the same bin,
84 /// and therefore will not be contending for the same lock.
85 ///
86 /// unordered_map_concurrent provides an iterator which points to an
87 /// entry in the map and also knows which bin it is in and implicitly
88 /// holds a lock on the bin. When the iterator is destroyed, the lock
89 /// on that bin is released. When the iterator is incremented in such a
90 /// way that it transitions from the last entry of its current bin to
91 /// the first entry of the next bin, it will also release its current
92 /// lock and obtain a lock on the next bin.
93 ///
94 
95 template<class KEY, class VALUE, class HASH = std::hash<KEY>,
96  class PRED = std::equal_to<KEY>, size_t BINS = 16,
97  class BINMAP = std::unordered_map<KEY, VALUE, HASH, PRED>>
99 public:
100  typedef BINMAP BinMap_t;
101  typedef typename BINMAP::iterator BinMap_iterator_t;
102  using key_type = KEY;
103 
104 public:
105  unordered_map_concurrent() { m_size = 0; }
106 
108  {
109  // for (size_t i = 0; i < BINS; ++i)
110  // std::cout << "Bin " << i << ": " << m_bins[i].map.size() << "\n";
111  }
112 
113  /// An unordered_map_concurrent::iterator points to a specific entry
114  /// in the umc, and holds a lock to the bin the entry is in.
115  class iterator {
116  public:
117  friend class unordered_map_concurrent<KEY, VALUE, HASH, PRED, BINS,
118  BINMAP>;
119 
120  public:
121  /// Construct an unordered_map_concurrent iterator that points
122  /// to nothing.
124  : m_umc(umc)
125  , m_bin(-1)
126  , m_locked(false)
127  {
128  }
129 
130  /// Copy constructor of an unordered_map_concurrent iterator
131  /// transfers the lock (if held) to this. Caveat: the copied
132  /// iterator no longer holds the lock!
134  {
135  m_umc = src.m_umc;
136  m_bin = src.m_bin;
137  m_biniterator = src.m_biniterator;
138  m_locked = src.m_locked;
139  // assignment transfers lock ownership
140  *(const_cast<bool*>(&src.m_locked)) = false;
141  }
142 
143  /// Destroying an unordered_map_concurrent iterator releases any
144  /// bin locks it held.
145  ~iterator() { clear(); }
146 
147  /// Totally invalidate this iterator -- point it to nothing
148  /// (releasing any locks it may have had).
149  void clear()
150  {
151  if (m_umc) {
152  unbin();
153  m_umc = NULL;
154  }
155  }
156 
157  // Dereferencing returns a reference to the hash table entry the
158  // iterator refers to.
159  const typename BinMap_t::value_type& operator*() const
160  {
161  return *m_biniterator;
162  }
163 
164  /// Dereferencing returns a reference to the hash table entry the
165  /// iterator refers to.
166  const typename BinMap_t::value_type* operator->() const
167  {
168  return &(*m_biniterator);
169  }
170 
171  /// Treating an iterator as a bool yields true if it points to a
172  /// valid element of one of the bins of the map, false if it's
173  /// equivalent to the end() iterator.
174  operator bool()
175  {
176  return m_umc && m_bin >= 0
177  && m_biniterator != m_umc->m_bins[m_bin].map.end();
178  }
179 
180  /// Iterator assignment transfers ownership of any bin locks
181  /// held by the operand.
183  {
184  unbin();
185  m_umc = src.m_umc;
186  m_bin = src.m_bin;
187  m_biniterator = src.m_biniterator;
188  m_locked = src.m_locked;
189  // assignment transfers lock ownership
190  *(const_cast<bool*>(&src.m_locked)) = false;
191  return *this;
192  }
193 
194  bool operator==(const iterator& other) const
195  {
196  if (m_umc != other.m_umc)
197  return false;
198  if (m_bin == -1 && other.m_bin == -1)
199  return true;
200  return m_bin == other.m_bin && m_biniterator == other.m_biniterator;
201  }
202  bool operator!=(const iterator& other) { return !(*this == other); }
203 
204  /// Increment to the next entry in the map. If we finish the
205  /// bin we're in, move on to the next bin (releasing our lock on
206  /// the old bin and acquiring a lock on the new bin). If we
207  /// finish the last bin of the map, return the end() iterator.
208  void operator++()
209  {
210  OIIO_DASSERT(m_umc);
211  OIIO_DASSERT(m_bin >= 0);
212  ++m_biniterator;
213  while (m_biniterator == m_umc->m_bins[m_bin].map.end()) {
214  if (m_bin == BINS - 1) {
215  // ran off the end
216  unbin();
217  return;
218  }
219  rebin(m_bin + 1);
220  }
221  }
222  void operator++(int) { ++(*this); }
223 
224  /// Lock the bin we point to, if not already locked.
225  void lock()
226  {
227  if (m_bin >= 0 && !m_locked) {
228  m_umc->m_bins[m_bin].lock();
229  m_locked = true;
230  }
231  }
232  /// Unlock the bin we point to, if locked.
233  void unlock()
234  {
235  if (m_bin >= 0 && m_locked) {
236  m_umc->m_bins[m_bin].unlock();
237  m_locked = false;
238  }
239  }
240 
241  /// Without changing the lock status (i.e., the caller already
242  /// holds the lock on the iterator's bin), increment to the next
243  /// element within the bin. Return true if it's pointing to a
244  /// valid element afterwards, false if it ran off the end of the
245  /// bin contents.
247  {
248  ++m_biniterator;
249  return (m_biniterator != m_umc->m_bins[m_bin].map.end());
250  }
251 
252  private:
253  // No longer refer to a particular bin, release lock on the bin
254  // it had (if any).
255  void unbin()
256  {
257  if (m_bin >= 0) {
258  if (m_locked)
259  unlock();
260  m_bin = -1;
261  }
262  }
263 
264  // Point this iterator to a different bin, releasing locks on
265  // the bin it previously referred to.
266  void rebin(int newbin)
267  {
268  OIIO_DASSERT(m_umc);
269  unbin();
270  m_bin = newbin;
271  lock();
272  m_biniterator = m_umc->m_bins[m_bin].map.begin();
273  }
274 
275  unordered_map_concurrent* m_umc; // which umc this iterator refers to
276  int m_bin; // which bin within the umc
277  BinMap_iterator_t m_biniterator; // which entry within the bin
278  bool m_locked; // do we own the lock on the bin?
279  };
280 
281 
282  /// Return an interator pointing to the first entry in the map.
284  {
285  iterator i(this);
286  i.rebin(0);
287  while (i.m_biniterator == m_bins[i.m_bin].map.end()) {
288  if (i.m_bin == BINS - 1) {
289  // ran off the end
290  i.unbin();
291  return i;
292  }
293  i.rebin(i.m_bin + 1);
294  }
295  return i;
296  }
297 
298  /// Return an iterator signifying the end of the map (no valid
299  /// entry pointed to).
301  {
302  iterator i(this);
303  return i;
304  }
305 
306  /// Search for key. If found, return an iterator referring to the
307  /// element, otherwise, return an iterator that is equivalent to
308  /// this->end(). If do_lock is true, lock the bin that we're
309  /// searching and return the iterator in a locked state, and unlock
310  /// the bin again if not found; however, if do_lock is false, assume
311  /// that the caller already has the bin locked, so do no locking or
312  /// unlocking and return an iterator that is unaware that it holds a
313  /// lock.
314  iterator find(const KEY& key, bool do_lock = true)
315  {
316  size_t hash = m_hash(key);
317  size_t b = whichbin(hash);
318  Bin& bin(m_bins[b]);
319  if (do_lock)
320  bin.lock();
321  auto it = find_with_hash(bin.map, key, hash);
322  if (it == bin.map.end()) {
323  // not found -- return the 'end' iterator
324  if (do_lock)
325  bin.unlock();
326  return end();
327  }
328  // Found
329  iterator i(this);
330  i.m_bin = (unsigned)b;
331  i.m_biniterator = it;
332  i.m_locked = do_lock;
333  return i;
334  }
335 
336  /// Search for key. If found, return an iterator referring to the
337  /// existing element, otherwise, insert the value and return an iterator
338  /// to the newly added element. If do_lock is true, lock the bin that
339  /// we're searching and return the iterator in a locked state; however,
340  /// if do_lock is false, assume that the caller already has the bin
341  /// locked, so do no locking and return an iterator that is unaware that
342  /// it holds a lock.
343  std::pair<iterator, bool> find_or_insert(const KEY& key, const VALUE& value,
344  bool do_lock = true)
345  {
346  size_t hash = m_hash(key);
347  size_t b = whichbin(hash);
348  bool inserted = false;
349  Bin& bin(m_bins[b]);
350  // We're returning an iterator no matter what, so prepare it
351  // partially now, before we are holding any lock.
352  iterator iret(this);
353  iret.m_bin = (unsigned)b;
354  iret.m_locked = do_lock;
355  if (do_lock)
356  bin.lock();
357  iret.m_biniterator = find_with_hash(bin.map, key, hash);
358  if (iret.m_biniterator == bin.map.end()) {
359  // Not found in the map, insert it
360  auto result = bin.map.emplace(key, value);
361  if (result.second) {
362  // the insert was successful!
363  ++m_size;
364  }
365  iret.m_biniterator = result.first;
366  inserted = true;
367  }
368  return { iret, inserted };
369  }
370 
371  /// Search for key. If found, return true and store the value. If not
372  /// found, return false and do not alter value. If do_lock is true,
373  /// read-lock the bin while we're searching, and release it before
374  /// returning; however, if do_lock is false, assume that the caller
375  /// already has the bin locked, so do no locking or unlocking.
376  bool retrieve(const KEY& key, VALUE& value, bool do_lock = true)
377  {
378  size_t hash = m_hash(key);
379  size_t b = whichbin(hash);
380  Bin& bin(m_bins[b]);
381  if (do_lock)
382  bin.read_lock();
383  auto it = find_with_hash(bin.map, key, hash);
384  bool found = (it != bin.map.end());
385  if (found)
386  value = it->second;
387  if (do_lock)
388  bin.read_unlock();
389  return found;
390  }
391 
392  /// Insert <key,value> into the hash map if it's not already there.
393  /// Return true if added, false if it was already present.
394  /// If it was already present in the map, replace `value` with the
395  /// value stored in the map.
396  /// If do_lock is true, lock the bin containing key while doing this
397  /// operation; if do_lock is false, assume that the caller already
398  /// has the bin locked, so do no locking or unlocking.
399  bool insert_retrieve(const KEY& key, VALUE& value, VALUE& mapvalue,
400  bool do_lock = true)
401  {
402  size_t hash = m_hash(key);
403  size_t b = whichbin(hash);
404  Bin& bin(m_bins[b]);
405  if (do_lock)
406  bin.lock();
407  auto result = bin.map.emplace(key, value);
408  if (result.second) {
409  // the insert was successful!
410  ++m_size;
411  } else {
412  // Replace caller's value with the one already in the table.
413  value = result.first->second;
414  }
415  if (do_lock)
416  bin.unlock();
417  return result.second;
418  }
419 
420  /// Insert <key,value> into the hash map if it's not already there.
421  /// Return true if added, false if it was already present.
422  /// If do_lock is true, lock the bin containing key while doing this
423  /// operation; if do_lock is false, assume that the caller already
424  /// has the bin locked, so do no locking or unlocking.
425  ///
426  /// N.B.: This method returns a bool, whereas std::unordered_map::insert
427  /// returns a pair<iterator,bool>. If you want the more standard
428  /// functionalty, we call that find_or_insert(). Sorry for the mixup,
429  /// it's too late to rename it now to conform to the standard.
430  bool insert(const KEY& key, const VALUE& value, bool do_lock = true)
431  {
432  size_t hash = m_hash(key);
433  size_t b = whichbin(hash);
434  Bin& bin(m_bins[b]);
435  if (do_lock)
436  bin.lock();
437  auto result = bin.map.emplace(key, value);
438  if (result.second) {
439  // the insert was successful!
440  ++m_size;
441  }
442  if (do_lock)
443  bin.unlock();
444  return result.second;
445  }
446 
447  /// If the key is in the map, safely erase it.
448  /// If do_lock is true, lock the bin containing key while doing this
449  /// operation; if do_lock is false, assume that the caller already
450  /// has the bin locked, so do no locking or unlocking.
451  void erase(const KEY& key, bool do_lock = true)
452  {
453  size_t hash = m_hash(key);
454  size_t b = whichbin(hash);
455  Bin& bin(m_bins[b]);
456  if (do_lock)
457  bin.lock();
458  bin.map.erase(key, hash);
459  --m_size;
460  if (do_lock)
461  bin.unlock();
462  }
463 
464  /// Return true if the entire map is empty.
465  bool empty() { return m_size == 0; }
466 
467  /// Return the total number of entries in the map.
468  size_t size() { return size_t(m_size); }
469 
470  /// Explicitly lock the bin that will contain the key (regardless of
471  /// whether there is such an entry in the map), and return its bin
472  /// number.
473  size_t lock_bin(const KEY& key)
474  {
475  size_t hash = m_hash(key);
476  size_t b = whichbin(hash);
477  m_bins[b].lock();
478  return b;
479  }
480 
481  /// Explicitly unlock the specified bin (this assumes that the caller
482  /// holds the lock).
483  void unlock_bin(size_t bin) { m_bins[bin].unlock(); }
484 
485  // Return a mask that is 1 for bits of the hash that are not used to
486  // determine the bin number.
487  static constexpr size_t nobin_mask() { return ~size_t(0) >> log2(BINS); }
488 
489 private:
490  struct Bin {
491  OIIO_CACHE_ALIGN // align bin to cache line
492  mutable spin_rw_mutex mutex; // mutex for this bin
493  BinMap_t map; // hash map for this bin
494 #ifndef NDEBUG
495  mutable atomic_int m_nrlocks; // for debugging
496  mutable atomic_int m_nwlocks; // for debugging
497 #endif
498 
499  Bin()
500  {
501 #ifndef NDEBUG
502  m_nrlocks = 0;
503  m_nwlocks = 0;
504 #endif
505  }
506  ~Bin()
507  {
508 #ifndef NDEBUG
509  OIIO_DASSERT(m_nrlocks == 0 && m_nwlocks == 0);
510 #endif
511  }
512 
513  void read_lock() const
514  {
515  mutex.read_lock();
516 #ifndef NDEBUG
517  ++m_nrlocks;
518  OIIO_DASSERT_MSG(m_nwlocks == 0,
519  "oops, m_nrlocks = %d, m_nwlocks = %d",
520  (int)m_nrlocks, (int)m_nwlocks);
521 #endif
522  }
523  void read_unlock() const
524  {
525 #ifndef NDEBUG
526  OIIO_DASSERT_MSG(m_nwlocks == 0 && m_nrlocks >= 1,
527  "oops, m_nrlocks = %d, m_nwlocks = %d",
528  (int)m_nrlocks, (int)m_nwlocks);
529  --m_nrlocks;
530 #endif
531  mutex.read_unlock();
532  }
533 
534  void lock() const
535  {
536  mutex.lock();
537 #ifndef NDEBUG
538  ++m_nwlocks;
539  OIIO_DASSERT_MSG(m_nwlocks == 1 && m_nrlocks == 0,
540  "oops, m_nrlocks = %d, m_nwlocks = %d",
541  (int)m_nrlocks, (int)m_nwlocks);
542 #endif
543  }
544  void unlock() const
545  {
546 #ifndef NDEBUG
547  OIIO_DASSERT_MSG(m_nwlocks == 1 && m_nrlocks == 0,
548  "oops, m_nrlocks = %d, m_nwlocks = %d",
549  (int)m_nrlocks, (int)m_nwlocks);
550  --m_nwlocks;
551 #endif
552  mutex.unlock();
553  }
554  };
555 
556  HASH m_hash; // hashing function
557  atomic_int m_size; // total entries in all bins
558  Bin m_bins[BINS]; // the bins
559 
560  static constexpr int log2(unsigned n)
561  {
562  return n < 2 ? 0 : 1 + log2(n / 2);
563  }
564 
565  // Which bin will this key always appear in?
566  size_t whichbin(size_t hash)
567  {
568  constexpr int LOG2_BINS = log2(BINS);
569  constexpr int BIN_SHIFT = 8 * sizeof(size_t) - LOG2_BINS;
570 
571  static_assert(1 << LOG2_BINS == BINS,
572  "Number of bins must be a power of two");
573  static_assert(~size_t(0) >> BIN_SHIFT == (BINS - 1), "Hash overflow");
574 
575  // Use the high order bits of the hash to index the bin. We assume that the
576  // low-order bits of the hash will directly be used to index the hash table,
577  // so using those would lead to collisions.
578  size_t bin = hash >> BIN_SHIFT;
579  OIIO_DASSERT(bin < BINS);
580  return bin;
581  }
582 };
583 
584 
void erase(const KEY &key, bool do_lock=true)
iterator & operator=(const iterator &src)
iterator(unordered_map_concurrent *umc=NULL)
static constexpr size_t nobin_mask()
#define OIIO_ENABLE_IF(...)
Definition: platform.h:645
bool retrieve(const KEY &key, VALUE &value, bool do_lock=true)
bool empty()
Return true if the entire map is empty.
size_t size()
Return the total number of entries in the map.
**But if you need a result
Definition: thread.h:613
bool insert_retrieve(const KEY &key, VALUE &value, VALUE &mapvalue, bool do_lock=true)
uint64 value_type
Definition: GA_PrimCompat.h:29
atomic< int > atomic_int
Definition: atomic.h:25
void unlock()
Unlock the bin we point to, if locked.
Wrappers and utilities for multithreading.
GLdouble n
Definition: glcorearb.h:2008
std::pair< iterator, bool > find_or_insert(const KEY &key, const VALUE &value, bool do_lock=true)
bool insert(const KEY &key, const VALUE &value, bool do_lock=true)
#define OIIO_DASSERT_MSG
Definition: dassert.h:56
#define OIIO_DASSERT
Definition: dassert.h:55
iterator begin()
Return an interator pointing to the first entry in the map.
size_t lock_bin(const KEY &key)
const BinMap_t::value_type * operator->() const
GLboolean GLboolean GLboolean b
Definition: glcorearb.h:1222
void lock()
Lock the bin we point to, if not already locked.
Map::iterator find_with_hash(Map &map, const Key &key, size_t hash)
#define OIIO_CACHE_ALIGN
Definition: platform.h:361
const BinMap_t::value_type & operator*() const
iterator find(const KEY &key, bool do_lock=true)
bool operator==(const iterator &other) const
Definition: core.h:1131
#define OIIO_NAMESPACE_END
Definition: oiioversion.h:94
type
Definition: core.h:1059
#define OIIO_NAMESPACE_BEGIN
Definition: oiioversion.h:93
GLenum src
Definition: glcorearb.h:1793