3 releases

0.1.2 Jun 28, 2023
0.1.1 Jun 28, 2023
0.1.0 Jun 28, 2023

#613 in Concurrency

Download history 2512/week @ 2024-07-19 2207/week @ 2024-07-26 2849/week @ 2024-08-02 2677/week @ 2024-08-09 2502/week @ 2024-08-16 2383/week @ 2024-08-23 2329/week @ 2024-08-30 2011/week @ 2024-09-06 2236/week @ 2024-09-13 1809/week @ 2024-09-20 1752/week @ 2024-09-27 2397/week @ 2024-10-04 2324/week @ 2024-10-11 2176/week @ 2024-10-18 2255/week @ 2024-10-25 1844/week @ 2024-11-01

9,097 downloads per month
Used in 2 crates (via qcell)

CC0 license

20KB
258 lines

Tests codecov

A lock-free concurrent set.

Tested with loom and miri.


lib.rs:

A lock-free concurrent set. The operations on this set are O(n) where n is the number of distinct values that have ever been inserted into the set.

The intended use-case is to provide a way to ensure exclusivity over critical sections of code on a per-value basis, where the number of distinct values is small but dynamic.

This data structure uses atomic singly-linked lists in two forms to enable its operations. One list has a node for every distinct value that has ever been inserted. The other type of list exists within each of those nodes, and manages a queue of threads waiting in the wait_to_insert method for another thread to call remove.

An atomic singly-linked list is relatively straightforward to insert to: Allocate a new node, and then in loop, update the 'next' pointer of the node to the most recent value of the 'head' pointer, and then attempt a compare-exchange, replacing the old 'head' with the pointer to the new node.

Things get more complicated as soon as you additionally consider removing items from the list. Anything that dereferences a node pointer now runs the risk of attempting to dereference a value which has been removed between the load that returned the pointer and the dereference of the pointer. Note that removal itself requires a dereference of the head pointer, to determine the value of head.next. This data structure avoids this issue in slightly different ways for the two different types of list.

The main list of nodes for each value avoids the issue by never removing nodes except in Drop. The exclusive access guarentee of Drop ensures that no other thread could attempt to access the list while it is being freed.

The list of waiting threads instead avoids the issue by specifying, for each list of waiting threads, which in the context of this set, means for each unique value, that at most one thread at a time may dereference a pointer. It exposes this contract as the safety requirement of the unsafe remove method. This requirement is easy to fulfil for applications where a value is only removed from the set by a logical "owner" which knows that it previously inserted a value.

Example

The following code inserts some values into the set, then removes one of them, and then spawns a second thread that waits to insert into the set.

After this code has been run, we can expect the data structure to look like this:

Dependencies

~0–27MB
~330K SLoC