You're probably reading this because you are looking for an AtomicUnorderedMap<K,V> that is fully general, highly concurrent (for reads, writes, and iteration), and makes no performance compromises. We haven't figured that one out yet. What you will find here is a hash table implementation that sacrifices generality so that it can give you all of the other things.
Declared in <folly/AtomicUnorderedMap.h>
template<
typename Key,
typename Value,
typename Hash = std::hash<Key>,
typename KeyEqual = std::equal_to<Key>,
bool SkipKeyValueDeletion = (std::is_trivially_destructible<Key>::value && std::is_trivially_destructible<Value>::value),
template<typename> typename Atom = atomic,
typename IndexType = uint32_t,
typename Allocator = /* implementation-defined */>
struct AtomicUnorderedInsertMap;
LIMITATIONS:
* Insert only (*) - the only write operation supported directly by AtomicUnorderedInsertMap is findOrConstruct. There is a (*) because values aren't moved, so you can roll your own concurrency control for in-place updates of values (see MutableData and MutableAtom below), but the hash table itself doesn't help you.
* No resizing - you must specify the capacity up front, and once the hash map gets full you won't be able to insert. Insert performance will degrade once the load factor is high. Insert is O(1/(1-actual_load_factor)). Note that this is a pretty strong limitation, because you can't remove existing keys.
* 2^30 maximum default capacity - by default AtomicUnorderedInsertMap uses uint32_t internal indexes (and steals 2 bits), limiting you to about a billion entries. If you need more you can fill in all of the template params so you change IndexType to uint64_t, or you can use AtomicUnorderedInsertMap64. 64-bit indexes will increase the space over of the map, of course.
WHAT YOU GET IN EXCHANGE:
* Arbitrary key and value types - any K and V that can be used in a std::unordered_map can be used here. In fact, the key and value types don't even have to be copyable or moveable!
* Keys and values in the map won't be moved - it is safe to keep pointers or references to the keys and values in the map, because they are never moved or destroyed (until the map itself is destroyed).
* Iterators are never invalidated - writes don't invalidate iterators, so you can scan and insert in parallel.
* Fast wait-free reads - reads are usually only a single cache miss, even when the hash table is very large. Wait-freedom means that you won't see latency outliers even in the face of concurrent writes.
* Lock-free insert - writes proceed in parallel. If a thread in the middle of a write is unlucky and gets suspended, it doesn't block anybody else.
COMMENTS ON INSERT-ONLY
This map provides wait-free linearizable reads and lock-free linearizable inserts. Inserted values won't be moved, but no concurrency control is provided for safely updating them. To remind you of that fact they are only provided in const form. This is the only simple safe thing to do while preserving something like the normal std::map iteration form, which requires that iteration be exposed via std::pair (and prevents encapsulation of access to the value).
There are a couple of reasonable policies for doing in-place concurrency control on the values. I am hoping that the policy can be injected via the value type or an extra template param, to keep the core AtomicUnorderedInsertMap insert-only:
CONST: this is the currently implemented strategy, which is simple, performant, and not that expressive. You can always put in a value with a mutable field (see MutableAtom below), but that doesn't look as pretty as it should.
ATOMIC: for integers and integer-size trivially copyable structs (via an adapter like tao/queues/AtomicStruct) the value can be a std::atomic and read and written atomically.
SEQ-LOCK: attach a counter incremented before and after write. Writers serialize by using CAS to make an even->odd transition, then odd->even after the write. Readers grab the value with memcpy, checking sequence value before and after. Readers retry until they see an even sequence number that doesn't change. This works for larger structs, but still requires memcpy to be equivalent to copy assignment, and it is no longer lock-free. It scales very well, because the readers are still invisible (no cache line writes).
LOCK: folly's SharedMutex would be a good choice here.
MEMORY ALLOCATION
Underlying memory is allocated as a big anonymous chunk. If the SkipKeyValueDeletion template param is true then deletion of the map consists of deallocating the backing memory, which is much faster than destructing all of the keys and values. Feel free to override if std::is_trivial_destructor isn't recognizing the triviality of your destructors.
| Name | Description |
|---|---|
ConstIterator | Const forward iterator over the key-value pairs in the map. |
| Name | Description |
|---|---|
const_iterator | Const iterator type over the map's key-value pairs. |
const_reference | A const reference to a value_type. |
difference_type | Signed integer type used for iterator differences. |
hasher | The hash function type. |
key_equal | The key equality comparison type. |
key_type | The key type stored in the map. |
mapped_type | The mapped value type stored in the map. |
size_type | Unsigned integer type used for sizes. |
value_type | The key-value pair type exposed by iterators. |
| Name | Description |
|---|---|
AtomicUnorderedInsertMap [constructor] | Constructs a map that will support the insertion of maxSize key-value pairs without exceeding the max load factor. Load factors of greater than 1 are not supported, and once the actual load factor of the map approaches 1 the insert performance will suffer. The capacity is limited to 2^30 (about a billion) for the default IndexType, beyond which we will throw invalid_argument. |
~AtomicUnorderedInsertMap [destructor] | Destroy the map, freeing the backing storage and any stored keys/values. |
begin | Return a const iterator to the first element in the map. |
cbegin | Return a const iterator to the first element in the map. |
cend | Return a const iterator past the last element in the map. |
emplace | This isn't really emplace, but it is what we need to test. Eventually we can duplicate all of the std::pair constructor forms, including a recursive tuple forwarding template http://functionalcpp.wordpress.com/2013/08/28/tuple-forwarding/). |
end | Return a const iterator past the last element in the map. |
find | Return an iterator to the element with the given key, or end() if absent. |
findOrConstruct | Searches for the key, returning (iter,false) if it is found. If it is not found calls the functor Func with a void* argument that is raw storage suitable for placement construction of a Value (see raw_value_type), then returns (iter,true). May call Func and then return (iter,false) if there are other concurrent writes, in which case the newly constructed value will be immediately destroyed. |
| Name | Description |
|---|---|
folly::AtomicUnorderedInsertMap::ConstIterator | Const forward iterator over the key-value pairs in the map. |