orx-concurrent-ordered-bag
An efficient, convenient and lightweight grow-only concurrent data structure allowing high performance and ordered concurrent collection.
- convenient:
ConcurrentOrderedBagcan safely be shared among threads simply as a shared reference. It is aPinnedConcurrentColwith a special concurrent state implementation. UnderlyingPinnedVecand concurrent bag can be converted back and forth to each other. The main goal of this collection is to enable efficient parallel operations with very simple implementations. - efficient:
ConcurrentOrderedBagis a lock free structure suitable for concurrent, copy-free and high performance growth while enabling to collect the results in the desired order.
Safety Requirements
Unlike ConcurrentBag and ConcurrentVec, collection into a CollectionOrderedBag is through unsafe setter methods which are flexible in allowing to write at any position of the bag at any order. In order to use the bag safely, the caller is expected to satisfy the following two safety requirements:
- Each position is written exactly once, so that there exists no race condition.
- At the point where
into_inneris called to get the underlying vector of collected elements, the bag must not contain any gaps.- Let
mbe the maximum index of the position that we write an element to. - The bag assumes that the length of the vector is equal to
m + 1. - Then, it expects that exactly
m + 1elements are written to the bag. - If the first condition was also satisfied; then, this condition is sufficient to conclude that the bag has no gaps and can be unwrapped.
- Let
Satisfying these two conditions is easy in certain situations and harder in others. A good idea in complicated cases is to pair ConcurrentOrderedBag with a ConcurrentIter to greatly mitigate complexity and safety risks, please see the parallel map example below.
Examples
Manual Example
In the following example, we split computation among two threads: the first thread processes inputs with even indices, and the second with odd indices. This fulfills the safety requirements mentioned above.
use *;
let n = 1024;
let evens_odds = new;
// just take a reference and share among threads
let bag = &evens_odds;
scope;
let vec = unsafe ;
assert_eq!;
for i in 0..n
Note that as long as no-gap and write-only-once guarantees are satisfied, ConcurrentOrderedBag is very flexible in the order of writes. Consider the following example. We spawn a thread just two write to the end of the collection. Then we spawn a bunch of other threads to fill the beginning of the collection. This just works without any locks or waits.
use *;
let n = 1024;
let num_additional_threads = 4;
let bag = new;
let con_bag = &bag;
scope;
let vec = unsafe ;
assert_eq!;
for i in 0..
assert_eq!;
These examples represent cases where the work can be trivially split among threads while providing the safety requirements. In a general case, it requires special care to fulfill the safety requirements. This complexity and safety risks can significantly be avoided by pairing the ConcurrentOrderedBag with a ConcurrentIter on the input side.
Parallel Map with ConcurrentIter
Parallel map operation is one of the cases where we care about the order of the collected elements, and hence, a ConcurrentBag would not do. On the other hand, a very simple yet efficient implementation can be achieved with ConcurrentOrderedBag and ConcurrentIter.
use *;
use *;
let len = 2465;
let input: = .map.collect;
let bag = parallel_map;
let output = unsafe ;
assert_eq!;
for in output.iter.enumerate
As you may see, no manual work or care is required to satisfy the safety requirements. Each element of the iterator is processed and written exactly once, just as it would in a sequential implementation.
Parallel Map with ExactSizeConcurrentIter
A further performance improvement to the parallel map implementation above is to distribute the tasks among the threads in chunks. The aim of this approach is to avoid false sharing, you may see further details here. This can be achieved by pairing an ExactSizeConcurrentIter rather than a ConcurrentIter with the set_values method of the ConcurrentOrderedBag.
use *;
use *;
let len = 2465;
let input: = .map.collect;
let bag = parallel_map;
let output = unsafe ;
for in output.iter.enumerate
Construction
ConcurrentOrderedBag can be constructed by wrapping any pinned vector; i.e., ConcurrentOrderedBag<T> implements From<P: PinnedVec<T>>. Likewise, a concurrent vector can be unwrapped without any allocation to the underlying pinned vector with into_inner method, provided that the safety requirements are satisfied.
Further, there exist with_ methods to directly construct the concurrent bag with common pinned vector implementations.
use *;
// default pinned vector -> SplitVec<T, Doubling>
let bag: = new;
let bag: = Defaultdefault;
let bag: = with_doubling_growth;
let bag: = with_doubling_growth;
let bag: = new.into;
let bag: = new.into;
// SplitVec with [Linear](https://docs.rs/orx-split-vec/latest/orx_split_vec/struct.Linear.html) growth
// each fragment will have capacity 2^10 = 1024
// and the split vector can grow up to 32 fragments
let bag: = with_linear_growth;
let bag: = with_linear_growth_and_fragments_capacity.into;
// [FixedVec](https://docs.rs/orx-fixed-vec/latest/orx_fixed_vec/) with fixed capacity.
// Fixed vector cannot grow; hence, pushing the 1025-th element to this bag will cause a panic!
let bag: = with_fixed_capacity;
let bag: = new.into;
Of course, the pinned vector to be wrapped does not need to be empty.
use *;
let split_vec: = .collect;
let bag: = split_vec.into;
Concurrent State and Properties
The concurrent state is modeled simply by an atomic capacity. Combination of this state and PinnedConcurrentCol leads to the following properties:
- Writing to a position of the collection does not block other writes, multiple writes can happen concurrently.
- Caller is required to guarantee that each position is written exactly once.
- ⟹ caller is responsible to avoid write & write race conditions.
- Only one growth can happen at a given time.
- Reading is only possible after converting the bag into the underlying
PinnedVec. - ⟹ no read & write race condition exists.
Concurrent Friend Collections
ConcurrentBag |
ConcurrentVec |
ConcurrentOrderedBag |
|
|---|---|---|---|
| Underlying Storage | Directly in a PinnedVec<T> |
Elements are wrapped with optional; i.e., stored in a PinnedVec<Option<T>> |
Directly in a PinnedVec<T> |
| Write | Guarantees that each element is written exactly once via push or extend methods |
Guarantees that each element is written exactly once via push or extend methods |
Different in two ways. First, a position can be written multiple times. Second, an arbitrary element of the bag can be written at any time at any order using set_value and set_values methods. This provides a great flexibility while moving the safety responsibility to the caller; hence, the set methods are unsafe. |
| Read | Mainly, a write-only collection. Concurrent reading of already pushed elements is through unsafe get and iter methods. The caller is required to avoid race conditions. |
A write-and-read collection. Already pushed elements can safely be read through get and iter methods. |
Not supported currently. Due to the flexible but unsafe nature of write operations, it is difficult to provide required safety guarantees as a caller. |
| Ordering of Elements | Since write operations are through adding elements to the end of the pinned vector via push and extend, two multi-threaded executions of a code that collects elements into the bag might result in the elements being collected in different orders. |
Since write operations are through adding elements to the end of the pinned vector via push and extend, two multi-threaded executions of a code that collects elements into the bag might result in the elements being collected in different orders. |
This is the main goal of this collection, allowing to collect elements concurrently and in the correct order. Although this does not seem trivial; it can be achieved almost trivially when ConcurrentOrderedBag is used together with a ConcurrentIter. |
into_inner |
Once the concurrent collection is completed, the bag can safely be converted to its underlying PinnedVec<T> without a cost. |
Once the concurrent collection is completed, the bag can safely be converted to its underlying PinnedVec<Option<T>> of option-wrapped elements without a cost. |
Growing through flexible setters allowing to write to any position, ConcurrentOrderedBag has the risk of containing gaps. into_inner call provides some useful metrics such as whether the number of elements pushed elements match the maximum index of the vector; however, it cannot guarantee that the bag is gap-free. The caller is required to take responsibility to unwrap to get the underlying PinnedVec<T> through an unsafe call. |
License
This library is licensed under MIT license. See LICENSE for details.