Skip to main content

flurry/
lib.rs

1//! A concurrent hash table based on Java's `ConcurrentHashMap`.
2//!
3//! A hash table that supports full concurrency of retrievals and high expected concurrency for
4//! updates. This type is functionally very similar to `std::collections::HashMap`, and for the
5//! most part has a similar API. Even though all operations on the map are thread-safe and operate
6//! on shared references, retrieval operations do *not* entail locking, and there is *not* any
7//! support for locking the entire table in a way that prevents all access.
8//!
9//! # Better Alternatives
10//!
11//! Flurry currently suffers performance and memory usage issues under load.
12//! You may wish to consider [`papaya`] or [`dashmap`] as alternatives if this is
13//! important to you.
14//!
15//! # A note on `Guard` and memory use
16//!
17//! You may have noticed that many of the access methods on this map take a reference to a
18//! [`Guard`]. The exact details of this are beyond the scope of this documentation (for
19//! that, see the [`seize`] crate), but some of the implications bear repeating here. You obtain a
20//! `Guard` using [`HashMap::guard`], and you can use references to the same guard to make multiple API
21//! calls if you wish. Whenever you get a reference to something stored in the map, that reference
22//! is tied to the lifetime of the `Guard` that you provided. This is because each `Guard` prevents
23//! the destruction of any item associated with it. Whenever something is read under a `Guard`,
24//! that something stays around for _at least_ as long as the `Guard` does. The map delays
25//! deallocating values until it safe to do so, and in order to amortize the cost of the necessary
26//! bookkeeping it may delay even further until there's a _batch_ of items that need to be
27//! deallocated.
28//!
29//! Notice that there is a trade-off here. Creating and dropping a `Guard` is not free, since it
30//! also needs to interact with said bookkeeping. But if you keep one around for a long time, you
31//! may accumulate much garbage which will take up valuable free memory on your system. Use your
32//! best judgement in deciding whether or not to re-use a `Guard`.
33//!
34//! # Consistency
35//!
36//! Retrieval operations (including [`get`](HashMap::get)) generally do not block, so may
37//! overlap with update operations (including [`insert`](HashMap::insert)). Retrievals
38//! reflect the results of the most recently *completed* update operations holding upon their
39//! onset. (More formally, an update operation for a given key bears a _happens-before_ relation
40//! with any successful retrieval for that key reporting the updated value.)
41//!
42//! Operations that inspect the map as a whole, rather than a single key, operate on a snapshot of
43//! the underlying table. For example, iterators return elements reflecting the state of the hash
44//! table at some point at or since the creation of the iterator. Aggregate status methods like
45//! [`len`](HashMap::len) are typically useful only when a map is not undergoing concurrent
46//! updates in other threads. Otherwise the results of these methods reflect transient states that
47//! may be adequate for monitoring or estimation purposes, but not for program control.
48//! Similarly, [`Clone`](std::clone::Clone) may not produce a "perfect" clone if the underlying
49//! map is being concurrently modified.
50//!
51//! # Resizing behavior
52//!
53//! The table is dynamically expanded when there are too many collisions (i.e., keys that have
54//! distinct hash codes but fall into the same slot modulo the table size), with the expected
55//! average effect of maintaining roughly two bins per mapping (corresponding to a 0.75 load factor
56//! threshold for resizing). There may be much variance around this average as mappings are added
57//! and removed, but overall, this maintains a commonly accepted time/space tradeoff for hash
58//! tables.  However, resizing this or any other kind of hash table may be a relatively slow
59//! operation. When possible, it is a good idea to provide a size estimate by using the
60//! [`with_capacity`](HashMap::with_capacity) constructor. Note that using many keys with
61//! exactly the same [`Hash`](std::hash::Hash) value is a sure way to slow down performance of any
62//! hash table. To ameliorate impact, keys are required to be [`Ord`](std::cmp::Ord). This is used
63//! by the map to more efficiently store bins that contain a large number of elements with
64//! colliding hashes using the comparison order on their keys.
65//!
66/*
67//! TODO: dynamic load factor
68//! */
69//! # Hash Sets
70//!
71//! Flurry also supports concurrent hash sets, which may be created through [`HashSet`]. Hash sets
72//! offer the same instantiation options as [`HashMap`], such as [`new`](HashSet::new) and
73//! [`with_capacity`](HashSet::with_capacity).
74//!
75/*
76//! TODO: frequency map through computeIfAbsent
77//!
78//! TODO: bulk operations like forEach, search, and reduce
79//! */
80//! # Implementation notes
81//!
82//! This data-structure is a pretty direct port of Java's `java.util.concurrent.ConcurrentHashMap`
83//! [from Doug Lea and the rest of the JSR166
84//! team](http://gee.cs.oswego.edu/dl/concurrency-interest/). Huge thanks to them for releasing the
85//! code into the public domain! Much of the documentation is also lifted from there. What follows
86//! is a slightly modified version of their implementation notes from within the [source
87//! file](http://gee.cs.oswego.edu/cgi-bin/viewcvs.cgi/jsr166/jsr166/src/main/java/util/concurrent/ConcurrentHashMap.java?revision=1.323&view=markup).
88//!
89//! The primary design goal of this hash table is to maintain concurrent readability (typically
90//! method [`get`](HashMap::get), but also iterators and related methods) while minimizing update contention.
91//! Secondary goals are to keep space consumption about the same or better than java.util.HashMap,
92//! and to support high initial insertion rates on an empty table by many threads.
93//!
94//! This map usually acts as a binned (bucketed) hash table.  Each key-value mapping is held in a
95//! `BinEntry`. Most nodes are of type `BinEntry::Node` with hash, key, value, and a `next` field.
96//! However, some other types of nodes exist: `BinEntry::TreeNode`s are arranged in balanced trees
97//! instead of linear lists. Bins of type `BinEntry::Tree` hold the roots of sets of `BinEntry::TreeNode`s.
98//! Some nodes are of type `BinEntry::Moved`; these "forwarding nodes" are placed at the
99//! heads of bins during resizing. The Java version also has other special node types, but these
100//! have not yet been implemented in this port. These special nodes are all either uncommon or
101//! transient.
102//!
103/*
104//! TODO: TreeNodes, ReservationNodes
105*/
106//! The table is lazily initialized to a power-of-two size upon the first insertion.  Each bin in
107//! the table normally contains a list of nodes (most often, the list has only zero or one
108//! `BinEntry`). Table accesses require atomic reads, writes, and CASes.
109//!
110//! Insertion (via `put`) of the first node in an empty bin is performed by just CASing it to the
111//! bin. This is by far the most common case for put operations under most key/hash distributions.
112//! Other update operations (insert, delete, and replace) require locks. We do not want to waste
113//! the space required to associate a distinct lock object with each bin, so we instead embed a
114//! lock inside each node, and use the lock in the the first node of a bin list as the lock for the
115//! bin.
116//!
117//! Using the first node of a list as a lock does not by itself suffice though: When a node is
118//! locked, any update must first validate that it is still the first node after locking it, and
119//! retry if not. Because new nodes are always appended to lists, once a node is first in a bin, it
120//! remains first until deleted or the bin becomes invalidated (upon resizing).
121//!
122//! The main disadvantage of per-bin locks is that other update operations on other nodes in a bin
123//! list protected by the same lock can stall, for example when user `Eq` implementations or
124//! mapping functions take a long time.  However, statistically, under random hash codes, this is
125//! not a common problem. Ideally, the frequency of nodes in bins follows a Poisson distribution
126//! (http://en.wikipedia.org/wiki/Poisson_distribution) with a parameter of about 0.5 on average,
127//! given the resizing threshold of 0.75, although with a large variance because of resizing
128//! granularity. Ignoring variance, the expected occurrences of list size `k` are `exp(-0.5) *
129//! pow(0.5, k) / factorial(k)`. The first values are:
130//!
131//! ```text
132//! 0:    0.60653066
133//! 1:    0.30326533
134//! 2:    0.07581633
135//! 3:    0.01263606
136//! 4:    0.00157952
137//! 5:    0.00015795
138//! 6:    0.00001316
139//! 7:    0.00000094
140//! 8:    0.00000006
141//! more: less than 1 in ten million
142//! ```
143//!
144//! Lock contention probability for two threads accessing distinct elements is roughly `1 / (8 *
145//! #elements)` under random hashes.
146//!
147//! Actual hash code distributions encountered in practice sometimes deviate significantly from
148//! uniform randomness. This includes the case when `N > (1<<30)`, so some keys MUST collide.
149//! Similarly for dumb or hostile usages in which multiple keys are designed to have identical hash
150//! codes or ones that differs only in masked-out high bits. So we use secondary strategy that
151//! applies when the number of nodes in a bin exceeds a threshold. These `BinEntry::Tree` bins use
152//! a balanced tree to hold nodes (a specialized form of red-black trees), bounding search time to
153//! `O(log N)`. Each search step in such a bin is at least twice as slow as in a regular list, but
154//! given that N cannot exceed `(1<<64)` (before running out of adresses) this bounds search steps,
155//! lock hold times, etc, to reasonable constants (roughly 100 nodes inspected per operation worst
156//! case). `BinEntry::Tree` nodes (`BinEntry::TreeNode`s) also maintain the same `next` traversal
157//! pointers as regular nodes, so can be traversed in iterators in a similar way.
158//!
159//! The table is resized when occupancy exceeds a percentage threshold (nominally, 0.75, but see
160//! below). Any thread noticing an overfull bin may assist in resizing after the initiating thread
161//! allocates and sets up the replacement array. However, rather than stalling, these other threads
162//! may proceed with insertions etc. The use of `BinEntry::Tree` bins shields us from the worst case
163//! effects of overfilling while resizes are in progress. Resizing proceeds by transferring bins,
164//! one by one, from the table to the next table. However, threads claim small blocks of indices to
165//! transfer (via the field `transfer_index`) before doing so, reducing contention. A generation
166//! stamp in the field `size_ctl` ensures that resizings do not overlap. Because we are using
167//! power-of-two expansion, the elements from each bin must either stay at same index, or move with
168//! a power of two offset. We eliminate unnecessary node creation by catching cases where old nodes
169//! can be reused because their next fields won't change. On average, only about one-sixth of them
170//! need cloning when a table doubles. The nodes they replace will be garbage collectible as soon
171//! as they are no longer referenced by any reader thread that may be in the midst of concurrently
172//! traversing table. Upon transfer, the old table bin contains only a special forwarding node
173//! (`BinEntry::Moved`) that contains the next table as its key. On encountering a forwarding node,
174//! access and update operations restart, using the new table.
175//!
176//! Each bin transfer requires its bin lock, which can stall waiting for locks while resizing.
177//! However, because other threads can join in and help resize rather than contend for locks,
178//! average aggregate waits become shorter as resizing progresses.  The transfer operation must
179//! also ensure that all accessible bins in both the old and new table are usable by any traversal.
180//! This is arranged in part by proceeding from the last bin `table.length - 1` up towards the
181//! first.  Upon seeing a forwarding node, traversals (see `iter::traverser::Traverser`) arrange to
182//! move to the new table without revisiting nodes.  To ensure that no intervening nodes are
183//! skipped even when moved out of order, a stack (see class `iter::traverser::TableStack`) is
184//! created on first encounter of a forwarding node during a traversal, to maintain its place if
185//! later processing the current table. The need for these save/restore mechanics is relatively
186//! rare, but when one forwarding node is encountered, typically many more will be. So `Traversers`
187//! use a simple caching scheme to avoid creating so many new `TableStack` nodes. (Thanks to Peter
188//! Levart for suggesting use of a stack here.)
189//!
190/* TODO:
191//!
192//! Lazy table initialization minimizes footprint until first use, and also avoids resizings when
193//! the first operation is from a `from_iter`, `From::from`, or deserialization. These cases
194//! attempt to override the initial capacity settings, but harmlessly fail to take effect in cases
195//! of races.
196*/
197/*
198//! TODO:
199//!
200//! The element count is maintained using a specialization of LongAdder. We need to incorporate a
201//! specialization rather than just use a LongAdder in order to access implicit contention-sensing
202//! that leads to creation of multiple CounterCells.  The counter mechanics avoid contention on
203//! updates but can encounter cache thrashing if read too frequently during concurrent access. To
204//! avoid reading so often, resizing under contention is attempted only upon adding to a bin
205//! already holding two or more nodes. Under uniform hash distributions, the probability of this
206//! occurring at threshold is around 13%, meaning that only about 1 in 8 puts check threshold (and
207//! after resizing, many fewer do so).
208//! */
209//!
210/* NOTE that we don't actually use most of the Java Code's complicated comparisons and tiebreakers
211 * since we require total ordering among the keys via `Ord` as opposed to a runtime check against
212 * Java's `Comparable` interface. */
213//! `BinEntry::Tree` bins use a special form of comparison for search and related operations (which
214//! is the main reason we cannot use existing collections such as tree maps). The contained tree
215//! is primarily ordered by hash value, then by [`cmp`](std::cmp::Ord::cmp) order on keys. The
216//! red-black balancing code is updated from pre-jdk collections (http://gee.cs.oswego.edu/dl/classes/collections/RBCell.java)
217//! based in turn on Cormen, Leiserson, and Rivest "Introduction to Algorithms" (CLR).
218//!
219//! `BinEntry::Tree` bins also require an additional locking mechanism. While list traversal is
220//! always possible by readers even during updates, tree traversal is not, mainly because of
221//! tree-rotations that may change the root node and/or its linkages. Tree bins include a simple
222//! read-write lock mechanism parasitic on the main bin-synchronization strategy: Structural
223//! adjustments associated with an insertion or removal are already bin-locked (and so cannot
224//! conflict with other writers) but must wait for ongoing readers to finish. Since there can be
225//! only one such waiter, we use a simple scheme using a single `waiter` field to block writers.
226//! However, readers need never block. If the root lock is held, they proceed along the slow
227//! traversal path (via next-pointers) until the lock becomes available or the list is exhausted,
228//! whichever comes first. These cases are not fast, but maximize aggregate expected throughput.
229//!
230//! ## Garbage collection
231//!
232//! The Java implementation can rely on Java's runtime garbage collection to safely deallocate
233//! deleted or removed nodes, keys, and values. Since Rust does not have such a runtime, we must
234//! ensure through some other mechanism that we do not drop values before all references to them
235//! have gone away. We do this using [`seize`], which provides a garbage collection scheme based
236//! on batch reference-counting. This forces us to make certain API changes such as requiring
237//! `Guard` arguments to many methods or wrapping the return values, but provides much more efficient
238//! operation than if every individual value had to be atomically reference-counted.
239//!
240//!  [`seize`]: https://docs.rs/seize
241//!  [`papaya`]: https://docs.rs/papaya
242//!  [`dashmap`]: https://docs.rs/dashmap
243#![deny(
244    missing_docs,
245    missing_debug_implementations,
246    unreachable_pub,
247    rustdoc::broken_intra_doc_links
248)]
249#![warn(rust_2018_idioms)]
250#![allow(clippy::cognitive_complexity)]
251
252mod map;
253mod map_ref;
254mod node;
255mod raw;
256mod reclaim;
257mod set;
258mod set_ref;
259
260#[cfg(feature = "rayon")]
261mod rayon_impls;
262
263#[cfg(feature = "serde")]
264mod serde_impls;
265
266/// Iterator types.
267pub mod iter;
268
269pub use map::{HashMap, TryInsertError};
270pub use map_ref::HashMapRef;
271pub use set::HashSet;
272pub use set_ref::HashSetRef;
273
274pub use seize::Guard;
275
276/// Default hash builder for [`HashMap`].
277// NOTE: This and the below exists solely to avoid ahash being part of the public flurry API,
278// so that we can bump the ahash major version without bumping flurry's major version.
279#[derive(Debug, Clone, Default)]
280#[repr(transparent)]
281pub struct DefaultHashBuilder(ahash::RandomState);
282
283/// Default hasher for [`HashMap`].
284#[derive(Debug, Clone)]
285#[repr(transparent)]
286pub struct DefaultHasher(ahash::AHasher);
287
288impl std::hash::BuildHasher for DefaultHashBuilder {
289    type Hasher = DefaultHasher;
290
291    fn build_hasher(&self) -> Self::Hasher {
292        DefaultHasher(self.0.build_hasher())
293    }
294
295    // NOTE: also implement hash_one so we can forward to ahash::RandomState's optimized impl.
296    fn hash_one<T: std::hash::Hash>(&self, x: T) -> u64
297    where
298        Self: Sized,
299    {
300        self.0.hash_one(x)
301    }
302}
303
304impl std::hash::Hasher for DefaultHasher {
305    fn finish(&self) -> u64 {
306        self.0.finish()
307    }
308
309    fn write(&mut self, bytes: &[u8]) {
310        self.0.write(bytes)
311    }
312
313    fn write_u8(&mut self, i: u8) {
314        self.0.write_u8(i)
315    }
316
317    fn write_u16(&mut self, i: u16) {
318        self.0.write_u16(i)
319    }
320
321    fn write_u32(&mut self, i: u32) {
322        self.0.write_u32(i)
323    }
324
325    fn write_u64(&mut self, i: u64) {
326        self.0.write_u64(i)
327    }
328
329    fn write_u128(&mut self, i: u128) {
330        self.0.write_u128(i)
331    }
332
333    fn write_usize(&mut self, i: usize) {
334        self.0.write_usize(i)
335    }
336
337    fn write_i8(&mut self, i: i8) {
338        self.0.write_i8(i)
339    }
340
341    fn write_i16(&mut self, i: i16) {
342        self.0.write_i16(i)
343    }
344
345    fn write_i32(&mut self, i: i32) {
346        self.0.write_i32(i)
347    }
348
349    fn write_i64(&mut self, i: i64) {
350        self.0.write_i64(i)
351    }
352
353    fn write_i128(&mut self, i: i128) {
354        self.0.write_i128(i)
355    }
356
357    fn write_isize(&mut self, i: isize) {
358        self.0.write_isize(i)
359    }
360}