concurrent_arena-0.1.10/.cargo_vcs_info.json0000644000000001360000000000100144430ustar { "git": { "sha1": "c9219b4df6112908ef3c3c03ac999d571169ae37" }, "path_in_vcs": "" }concurrent_arena-0.1.10/.github/dependabot.yml000064400000000000000000000004361046102023000174260ustar 00000000000000version: 2 updates: - package-ecosystem: "github-actions" # Workflow files stored in the # default location of `.github/workflows` directory: "/" schedule: interval: "daily" - package-ecosystem: "cargo" directory: "/" schedule: interval: "daily" concurrent_arena-0.1.10/.github/workflows/minimal.yml000064400000000000000000000014631046102023000210050ustar 00000000000000name: Minimal env: RUSTFLAGS: -Dwarnings RUST_BACKTRACE: 1 CARGO_TERM_COLOR: always CARGO_INCREMENTAL: 0 CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse CARGO_UNSTABLE_SPARSE_REGISTRY: true on: push: paths-ignore: - 'README.md' - 'LICENSE' - '.gitignore' pull_request: paths-ignore: - 'README.md' - 'LICENSE' - '.gitignore' jobs: minimal: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install nightly rust run: rustup toolchain install nightly --no-self-update --profile minimal - name: Create Cargo.lock containing the minimal versions run: cargo +nightly update -Zminimal-versions - uses: Swatinem/rust-cache@v2 - name: Check with minimal version of deps run: cargo check --lib --locked concurrent_arena-0.1.10/.github/workflows/miri.yml000064400000000000000000000020351046102023000203130ustar 00000000000000name: Miri env: RUSTFLAGS: -Dwarnings RUST_BACKTRACE: 1 CARGO_TERM_COLOR: always CARGO_INCREMENTAL: 0 CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse CARGO_UNSTABLE_SPARSE_REGISTRY: true on: push: branches: [none] paths-ignore: - 'README.md' - 'LICENSE' - '.gitignore' #pull_request: # paths-ignore: # - 'README.md' # - 'LICENSE' # - '.gitignore' jobs: miri: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install latest nightly run: | rustup toolchain install nightly --component miri --no-self-update --profile minimal rustup default nightly - uses: taiki-e/install-action@v2 with: tool: cargo-nextest - uses: Swatinem/rust-cache@v2 - name: Miri run: | cargo +nightly miri \ nextest run \ -Z build-std \ --target "$(rustc -vV | grep host | cut -d : -f 2 | tr -d '[:space:]')" \ --release env: MIRIFLAGS: -Zmiri-disable-isolation concurrent_arena-0.1.10/.github/workflows/msrv.yml000064400000000000000000000014271046102023000203460ustar 00000000000000name: Msrv env: RUSTFLAGS: -Dwarnings RUST_BACKTRACE: 1 CARGO_TERM_COLOR: always CARGO_INCREMENTAL: 0 CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse CARGO_UNSTABLE_SPARSE_REGISTRY: true on: push: paths-ignore: - 'README.md' - 'LICENSE' - '.gitignore' pull_request: paths-ignore: - 'README.md' - 'LICENSE' - '.gitignore' jobs: msrv: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install rust 1.63 run: | rustup toolchain install 1.63 nightly --no-self-update --profile minimal rustup default 1.63 - name: Use minimal versions run: cargo +nightly update -Zminimal-versions - uses: Swatinem/rust-cache@v2 - name: Check msrv run: cargo check --lib concurrent_arena-0.1.10/.github/workflows/release-plz.yml000064400000000000000000000011221046102023000215720ustar 00000000000000name: Release-plz permissions: pull-requests: write contents: write on: push: branches: - main jobs: release-plz: name: Release-plz runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Run release-plz uses: MarcoIeni/release-plz-action@v0.5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} concurrent_arena-0.1.10/.github/workflows/rust.yml000064400000000000000000000050651046102023000203560ustar 00000000000000name: Rust env: RUSTFLAGS: -Dwarnings RUST_BACKTRACE: 1 CARGO_TERM_COLOR: always CARGO_INCREMENTAL: 0 CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse CARGO_UNSTABLE_SPARSE_REGISTRY: true on: push: paths-ignore: - 'README.md' - 'LICENSE' - '.gitignore' pull_request: paths-ignore: - 'README.md' - 'LICENSE' - '.gitignore' jobs: check_format: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Check format run: cargo fmt --all -- --check run_clippy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 - name: Run clippy run: cargo clippy --all --all-features --no-deps test-32bit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: taiki-e/install-action@v2 with: tool: nextest - run: rustup target add i686-unknown-linux-musl - uses: Swatinem/rust-cache@v2 - run: | cargo nextest run --target i686-unknown-linux-musl cargo test --doc --target i686-unknown-linux-musl test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 - name: Run tests run: cargo test test-release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 - name: Run tests run: cargo test --release test-sanitize-address: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install latest nightly run: | rustup toolchain install nightly --no-self-update --profile minimal rustup default nightly - uses: Swatinem/rust-cache@v2 - name: Run tests run: cargo +nightly test env: RUSTFLAGS: -Zsanitizer=address ${{ env.RUSTFLAGS }} RUSTDOCFLAGS: -Zsanitizer=address ${{ env.RUSTFLAGS }} test-sanitize-thread: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install latest nightly run: | rustup toolchain install nightly --component rust-src --no-self-update --profile minimal rustup default nightly - uses: Swatinem/rust-cache@v2 - name: Run tests run: | cargo +nightly test \ -Z build-std \ --target "$(rustc -vV | grep host | cut -d : -f 2 | tr -d '[:space:]')" \ --features thread-sanitizer env: RUSTFLAGS: -Zsanitizer=thread ${{ env.RUSTFLAGS }} RUSTDOCFLAGS: -Zsanitizer=thread ${{ env.RUSTFLAGS }} concurrent_arena-0.1.10/.gitignore000064400000000000000000000006311046102023000152230ustar 00000000000000# Generated by Cargo # will have compiled files and executables /target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk # Added by cargo # # already existing elements were commented out /target #Cargo.lock concurrent_arena-0.1.10/CHANGELOG.md000064400000000000000000000017561046102023000150550ustar 00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.1.10](https://github.com/NobodyXu/concurrent_arena/compare/v0.1.9...v0.1.10) - 2024-10-19 ### Other - Fix BitMap::new clippy: Do not create constant for type with interior… ([#22](https://github.com/NobodyXu/concurrent_arena/pull/22)) - Use exponential growth for O(1) amortizied complexity ([#20](https://github.com/NobodyXu/concurrent_arena/pull/20)) ## [0.1.9](https://github.com/NobodyXu/concurrent_arena/compare/v0.1.8...v0.1.9) - 2024-09-11 ### Other - Add release-plz.yml ([#18](https://github.com/NobodyXu/concurrent_arena/pull/18)) - Fix tests on 32 bit ([#17](https://github.com/NobodyXu/concurrent_arena/pull/17)) - Bump actions/checkout from 3 to 4 ([#14](https://github.com/NobodyXu/concurrent_arena/pull/14)) concurrent_arena-0.1.10/Cargo.toml0000644000000023550000000000100124460ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" rust-version = "1.63" name = "concurrent_arena" version = "0.1.10" build = false autobins = false autoexamples = false autotests = false autobenches = false description = "u32 concurrent insertion/removal arena that returns ArenaArc" readme = "README.md" keywords = [ "concurrency", "arena", "shared", "slotmap", ] categories = ["concurrency"] license = "MIT" repository = "https://github.com/NobodyXu/concurrent_arena" [lib] name = "concurrent_arena" path = "src/lib.rs" [dependencies.arc-swap] version = "1.5.0" [dependencies.parking_lot] version = "0.12.0" [dependencies.triomphe] version = "0.1.5" features = ["arc-swap"] [dev-dependencies.bitvec] version = "1.0" [dev-dependencies.rayon] version = "1.5.1" [features] thread-sanitizer = [] concurrent_arena-0.1.10/Cargo.toml.orig000064400000000000000000000010301046102023000161140ustar 00000000000000[package] name = "concurrent_arena" version = "0.1.10" edition = "2018" rust-version = "1.63" license = "MIT" description = "u32 concurrent insertion/removal arena that returns ArenaArc" repository = "https://github.com/NobodyXu/concurrent_arena" keywords = ["concurrency", "arena", "shared", "slotmap"] categories = ["concurrency"] [features] thread-sanitizer = [] [dependencies] parking_lot = "0.12.0" triomphe = { version = "0.1.5", features = ["arc-swap"] } arc-swap = "1.5.0" [dev-dependencies] bitvec = "1.0" rayon = "1.5.1" concurrent_arena-0.1.10/LICENSE000064400000000000000000000020521046102023000142370ustar 00000000000000MIT License Copyright (c) 2021 Jiahao XU Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. concurrent_arena-0.1.10/README.md000064400000000000000000000013201046102023000145060ustar 00000000000000# ConcurrentArena [![Rust](https://github.com/NobodyXu/concurrent_arena/actions/workflows/rust.yml/badge.svg)](https://github.com/NobodyXu/concurrent_arena/actions/workflows/rust.yml) [![crate.io downloads](https://img.shields.io/crates/d/concurrent_arena)](https://crates.io/crates/concurrent_arena) [![crate.io version](https://img.shields.io/crates/v/concurrent_arena)](https://crates.io/crates/concurrent_arena) [![docs](https://docs.rs/concurrent_arena/badge.svg)](https://docs.rs/concurrent_arena) Concurrent arena that - Support concurrent inserted and removed; - Use a `u32` as key; - Returns `ArenaArc` to track the inserted object to avoid lifetime issues. ## How to run tests ``` ./run_tests.sh ``` concurrent_arena-0.1.10/run_tests.sh000075500000000000000000000015431046102023000156230ustar 00000000000000#!/bin/bash set -euxo pipefail cd "$(dirname "$(realpath "$0")")" export RUST_TEST_THREADS=1 rep=$(seq 1 10) for _ in $rep; do # shellcheck disable=SC2068 cargo test $@ -- --nocapture done export RUSTFLAGS='-Zsanitizer=address' export RUSTDOCFLAGS="$RUSTFLAGS" for _ in $rep; do # shellcheck disable=SC2068 cargo +nightly test $@ -- --nocapture done export RUSTFLAGS='-Zsanitizer=thread' export RUSTDOCFLAGS="$RUSTFLAGS" target="$(rustc -vV | grep host | cut -d : -f 2 | tr -d '[:space:]')" for _ in $rep; do # shellcheck disable=SC2068 cargo +nightly test $@ \ -Z build-std \ --target "$target" \ --features thread-sanitizer \ -- --nocapture done #export MIRIFLAGS="-Zmiri-disable-isolation" #exec cargo +nightly miri \ # nextest run \ # -Z build-std \ # --target "$target" \ # --release concurrent_arena-0.1.10/src/arcs.rs000064400000000000000000000120211046102023000153140ustar 00000000000000#![forbid(unsafe_code)] use core::{marker::PhantomData, ops::Deref, slice::Iter}; use arc_swap::{ArcSwapAny, Guard}; use parking_lot::Mutex; use triomphe::ThinArc; #[derive(Debug)] pub(crate) struct Arcs { array: ArcSwapAny>>, mutex: Mutex<()>, } impl Arcs { pub(crate) fn new() -> Self { Self { array: ArcSwapAny::new(None), mutex: Mutex::new(()), } } pub(crate) fn as_slice(&self) -> Slice<'_, T> { Slice(self.array.load(), PhantomData) } pub(crate) fn len(&self) -> usize { self.as_slice().len() } pub(crate) fn is_empty(&self) -> bool { self.len() == 0 } } impl Arcs { pub(crate) fn grow(&self, new_len: usize, f: impl FnMut() -> T) { if self.len() < new_len { let _guard = self.mutex.lock(); self.do_grow(new_len, f); } } /// This function is technically lock-free despite the fact that `self.mutex` is /// used, since it only `try_lock` the mutex. pub(crate) fn try_grow(&self, new_len: usize, f: impl FnMut() -> T) -> Result<(), ()> { if self.len() < new_len { if let Some(_guard) = self.mutex.try_lock() { self.do_grow(new_len, f); Ok(()) } else { Err(()) } } else { Ok(()) } } fn do_grow(&self, new_len: usize, f: impl FnMut() -> T) { let slice = self.as_slice(); let old_len = slice.len(); if old_len >= new_len { return; } struct Initializer<'a, T, F>(Iter<'a, T>, usize, F); impl T> Iterator for Initializer<'_, T, F> { type Item = T; fn next(&mut self) -> Option { if let Some(val) = self.0.next() { Some(val.clone()) } else if self.1 != 0 { self.1 -= 1; Some(self.2()) } else { None } } fn size_hint(&self) -> (usize, Option) { let len = self.0.len() + self.1; (len, Some(len)) } } impl T> ExactSizeIterator for Initializer<'_, T, F> {} let arc = ThinArc::from_header_and_iter((), Initializer(slice.iter(), new_len - old_len, f)); let _old = self.array.swap(Some(arc)); #[cfg(debug_assertions)] debug_assert!(slice.is_same_arc(_old.as_ref())); } } /// Slice is just a temporary borrow of the object. pub(crate) struct Slice<'a, T>(Guard>>, PhantomData<&'a Arcs>); impl Slice<'_, T> { #[cfg(debug_assertions)] fn is_same_arc(&self, other: Option<&ThinArc<(), T>>) -> bool { let this = self.0.as_ref(); if this.is_none() && other.is_none() { return true; } let this = if let Some(this) = this { this } else { return false; }; let other = if let Some(other) = other { other } else { return false; }; this.heap_ptr() == other.heap_ptr() } } impl Deref for Slice<'_, T> { type Target = [T]; fn deref(&self) -> &Self::Target { self.0 .as_ref() .map(ThinArc::deref) .map(|header_slice| &header_slice.slice) .unwrap_or(&[]) } } /// Thread sanitizer produces false positive in this test. /// /// This has been discussed in /// [this issue](https://github.com/vorner/arc-swap/issues/71) /// and the failure can only be reproduced on x86-64-unknown-linux-gnu. /// It cannot be reproduced on MacOS. /// /// Since crate arc-swap is a cross platform crate with no assembly used /// or any x86 specific feature, this can be some bugs in the allocator /// or the thread sanitizer. #[cfg(not(feature = "thread-sanitizer"))] #[cfg(test)] mod tests { use super::Arcs; use parking_lot::Mutex; use std::sync::Arc; use rayon::prelude::*; #[test] fn test() { let bag: Arc>>> = Arc::new(Arcs::new()); assert_eq!(bag.len(), 0); assert!(bag.is_empty()); { let slice = bag.as_slice(); assert!(slice.is_empty()); assert_eq!(slice.len(), 0); } bag.grow(10, Arc::default); { let slice = bag.as_slice(); assert!(!slice.is_empty()); for (i, arc) in slice.iter().enumerate() { *arc.lock() = i as u32; } } let bag_cloned = bag.clone(); (0..u8::MAX).into_par_iter().for_each(move |_i| { bag_cloned.grow(bag_cloned.len() + 32, Arc::default); }); { let slice = bag.as_slice(); assert!(!slice.is_empty()); for (i, arc) in slice.iter().take(10).enumerate() { *arc.lock() = i as u32; } } } } concurrent_arena-0.1.10/src/arena.rs000064400000000000000000000234031046102023000154600ustar 00000000000000use super::{arcs::Arcs, bucket::Bucket, thread_id::get_thread_id, Arc, ArenaArc}; use core::cmp::min; /// * `LEN` - Number of elements stored per bucket. /// Must be less than or equal to `u32::MAX`, divisible by /// `usize::BITS` and it must not be `0`. /// * `BITARRAY_LEN` - Number of [`usize`] in the bitmap per bucket. /// Must be equal to `LEN / usize::BITS`. /// /// For best performance, try to set this to number of CPUs that are going /// to access `Arena` concurrently. /// /// `Arena` stores the elements in buckets to ensure that the address /// for elements are stable while improving efficiency. /// /// Every bucket is of size `LEN`. /// /// The larger `LEN` is, the more compact the `Arena` will be, however it might /// also waste space if it is unused. /// /// And, allocating a large chunk of memory takes more time. /// /// `Arena` internally stores the array of buckets as a `triomphe::ThinArc` /// and use `ArcSwapAny` to grow the array atomically, without blocking any /// reader. /// /// # Examples /// /// If you provides `Arena` with invalid `LEM` or `BITARRAY_LEN`, then your /// code will panic at runtime: /// /// ```rust,should_panic /// use concurrent_arena::*; /// let arena = Arena::::new(); /// ``` /// /// To make it a compile time failure, you need to call /// `max_buckets`: /// /// ```rust,compile_fail /// use concurrent_arena::*; /// const MAX_BUCKETS: u32 = Arena::::max_buckets(); /// ``` #[derive(Debug)] pub struct Arena { buckets: Arcs>>, } impl Default for Arena { fn default() -> Self { Self::new() } } const fn check_const_generics() { let bits = usize::BITS as usize; assert!( LEN <= (u32::MAX as usize), "LEN must be less than or equal to u32::MAX" ); assert!(LEN % bits == 0, "LEN must be divisible by usize::BITS"); assert!(LEN != 0, "LEN must not be 0"); assert!( LEN / bits == BITARRAY_LEN, "BITARRAY_LEN must be equal to LEN / usize::BITS" ); } impl Arena { /// Maximum buckets `Arena` can have. pub const fn max_buckets() -> u32 { check_const_generics::(); u32::MAX / (LEN as u32) } } impl Arena { /// Would preallocate 2 buckets. pub fn new() -> Self { Self::with_capacity(2) } pub fn with_capacity(cap: u32) -> Self { check_const_generics::(); let cap = min(cap, Self::max_buckets()); let buckets = Arcs::new(); buckets.grow(cap as usize, Arc::default); Self { buckets } } /// Return Ok(arc) on success, or Err((value, len)) where value is /// the input param `value` and `len` is the length of the `Arena` at the time /// of insertion. /// /// This function is lock-free. pub fn try_insert(&self, mut value: T) -> Result, (T, u32)> { let slice = self.buckets.as_slice(); let len = slice.len(); debug_assert!(len <= Self::max_buckets() as usize); if len == 0 { return Err((value, 0)); } let mut pos = get_thread_id() % len; let slice1_iter = slice[pos..].iter(); let slice2_iter = slice[..pos].iter(); for bucket in slice1_iter.chain(slice2_iter) { match Bucket::try_insert(bucket, pos as u32, value) { Ok(arc) => return Ok(arc), Err(val) => value = val, } pos = (pos + 1) % len; } Err((value, len as u32)) } /// Try to reserve `min(new_len, Self::max_buckets())` buckets. /// /// This function is technically lock-free. pub fn try_reserve(&self, new_len: u32) -> bool { if new_len == 0 { return true; } let new_len = min(new_len, Self::max_buckets()); self.buckets .try_grow(new_len as usize, Arc::default) .is_ok() } /// Reserve `min(new_len, Self::max_buckets())` buckets. pub fn reserve(&self, new_len: u32) { if new_len != 0 { let new_len = min(new_len, Self::max_buckets()); self.buckets.grow(new_len as usize, Arc::default) } } /// Insert one value. /// /// If there isn't enough buckets, then try to reserve one bucket and /// restart the operation. pub fn insert(&self, mut value: T) -> ArenaArc { // Fast path where `try_reserve` is used to avoid locking. for _ in 0..5 { match self.try_insert(value) { Ok(arc) => return arc, Err((val, len)) => { value = val; // If len == Self::max_buckets(), then we would have to // wait for slots to be removed from `Arena`. if len != Self::max_buckets() { // If try_reserve succeeds, then another new bucket is available. // // If try_reserve fail, then another thread is doing the // reservation. // // We can simply restart operation, waiting for it to be done. // // Grow by 1.5 exponential to have amoritized O(1), adding +4 more in case // there's only one element (1 * 3 / 2 evaluaes to 1 in rust). self.try_reserve(len * 3 / 2 + 4); } } } } // Slow path where `reserve` is used. loop { match self.try_insert(value) { Ok(arc) => break arc, Err((val, len)) => { value = val; // If len == Self::max_buckets(), then we would have to // wait for slots to be removed from `Arena`. if len != Self::max_buckets() { self.reserve(len + 8); } } } } } } type AccessOp = unsafe fn( Arc>, u32, u32, ) -> Option>; impl Arena { fn access_impl( &self, slot: u32, op: AccessOp, ) -> Option> { let bucket_index = slot / (LEN as u32); let index = slot % (LEN as u32); self.buckets .as_slice() .get(bucket_index as usize) .cloned() // Safety: index is <= LEN .and_then(|bucket| unsafe { op(bucket, bucket_index, index) }) } /// May enter busy loop if the slot is not fully initialized. /// /// This function is lock free. pub fn remove(&self, slot: u32) -> Option> { self.access_impl(slot, Bucket::remove) } /// May enter busy loop if the slot is not fully initialized. /// /// This function is lock free. pub fn get(&self, slot: u32) -> Option> { self.access_impl(slot, Bucket::get) } /// Return number of buckets allocated. /// /// This function is lock free. pub fn len(&self) -> u32 { self.buckets.len() as u32 } /// This function is lock free. pub fn is_empty(&self) -> bool { self.buckets.is_empty() } } #[cfg(test)] mod tests { use crate::*; const LEN: usize = usize::BITS as usize; #[test] fn test_new() { let arena: Arena<_, 1, { LEN }> = Arena::new(); let slot = ArenaArc::slot(&arena.insert(())); assert_eq!(ArenaArc::slot(&arena.remove(slot).unwrap()), slot); } #[test] fn test_with_capacity() { let arena: Arena<_, 1, { LEN }> = Arena::with_capacity(0); let slot = ArenaArc::slot(&arena.insert(())); assert_eq!(ArenaArc::slot(&arena.remove(slot).unwrap()), slot); } /// Thread sanitizer produces false positive in this test. /// /// This has been discussed in /// [this issue](https://github.com/vorner/arc-swap/issues/71) /// and the failure can only be reproduced on x86-64-unknown-linux-gnu. /// It cannot be reproduced on MacOS. /// /// Since crate arc-swap is a cross platform crate with no assembly used /// or any x86 specific feature, this can be some bugs in the allocator /// or the thread sanitizer. #[cfg(not(feature = "thread-sanitizer"))] #[test] fn realworld_test() { use std::thread::sleep; use std::time::Duration; use parking_lot::Mutex; use rayon::prelude::*; use rayon::spawn; use std::sync::Arc; let arena: Arc, 1, { LEN }>> = Arc::new(Arena::with_capacity(0)); (0..u16::MAX).into_par_iter().for_each(|i| { let i = i as u32; let arc = arena.insert(Mutex::new(i)); assert_eq!(ArenaArc::strong_count(&arc), 2); assert_eq!(*arc.lock(), i); let slot = ArenaArc::slot(&arc); let arena = arena.clone(); spawn(move || { sleep(Duration::from_micros(1)); let arc = arena.remove(slot).unwrap(); let mut guard = arc.lock(); assert_eq!(*guard, i); *guard = 2000; }); }); } } concurrent_arena-0.1.10/src/bitmap.rs000064400000000000000000000122241046102023000156450ustar 00000000000000use super::{thread_id::get_thread_id, SliceExt}; use std::{ array, sync::atomic::{AtomicUsize, Ordering::Relaxed}, }; fn compare_exchange(atomic: &AtomicUsize, curr: usize, new: usize) -> Result<(), usize> { atomic .compare_exchange_weak(curr, new, Relaxed, Relaxed) .map(|_| ()) } /// * `BITARRAY_LEN` - the number of AtomicUsize #[derive(Debug)] pub(crate) struct BitMap([AtomicUsize; BITARRAY_LEN]); impl BitMap { pub(crate) fn new() -> Self { Self(array::from_fn(|_| AtomicUsize::new(0))) } /// # Safety /// /// `index` <= `BITARRAY_LEN / usize::BITS` pub(crate) unsafe fn load(&self, index: u32) -> bool { let bits = usize::BITS; let mask = 1 << (index % bits); let offset = (index / bits) as usize; (self.0.get_unchecked_on_release(offset).load(Relaxed) & mask) != 0 } pub(crate) fn allocate(&self) -> Option { let bits = usize::BITS as usize; let mut pos = if BITARRAY_LEN == bits { 0 } else { get_thread_id() % BITARRAY_LEN }; let slice1_iter = self.0[pos..].iter(); let slice2_iter = self.0[..pos].iter(); for chunk in slice1_iter.chain(slice2_iter) { let mut value = chunk.load(Relaxed); loop { if value == usize::MAX { break; } for i in 0..bits { let mask = 1 << i; if (value & mask) != 0 { continue; } match compare_exchange(chunk, value, value | mask) { Ok(_) => { return Some(pos * bits + i); } Err(new_value) => { value = new_value; // try again break; } } } } pos = (pos + 1) % BITARRAY_LEN; } None } /// # Safety /// /// `index` <= `BITARRAY_LEN / usize::BITS` pub(crate) unsafe fn deallocate(&self, index: usize) { let bits = usize::BITS as usize; let chunk = self.0.get_unchecked_on_release(index / bits); let mask = !(1 << (index % bits)); chunk.fetch_and(mask, Relaxed); } #[cfg(test)] pub(crate) fn is_all_one(&self) -> bool { self.0.iter().all(|each| each.load(Relaxed) == usize::MAX) } } #[cfg(test)] mod tests { use super::BitMap; use parking_lot::Mutex; use std::sync::Arc; use bitvec::prelude::*; use std::thread::sleep; use std::time::Duration; use rayon::prelude::*; const LEN: usize = 512; #[test] fn test() { let bits = usize::BITS as usize; let mut bitvec = BitVec::::with_capacity(LEN * bits); bitvec.resize(LEN * bits, false); assert_eq!(bitvec.len(), LEN * bits); assert_eq!(bitvec.count_ones(), 0); let arc = Arc::new(( BitMap::::new(), Mutex::new(bitvec.into_boxed_bitslice()), )); let max_index = (LEN * bits) as usize; let arc_cloned = arc.clone(); (0..(LEN * bits)).into_par_iter().for_each(|_| { let index = arc_cloned.0.allocate().unwrap(); assert!(index <= max_index); assert!(unsafe { arc_cloned.0.load(index as u32) }); assert!(!arc_cloned.1.lock().get_mut(index).unwrap().replace(true)); }); let bitmap = &arc.0; let bitvec = arc.1.lock(); assert_eq!(bitvec.count_zeros(), 0); assert!(bitmap.is_all_one()); assert!(bitmap.allocate().is_none()); for i in 0..(LEN * bits) { assert!(unsafe { bitmap.load(i as u32) }); unsafe { bitmap.deallocate(i) }; assert!(!unsafe { bitmap.load(i as u32) }); let index = bitmap.allocate().unwrap(); assert_eq!(index, i); assert!(unsafe { bitmap.load(i as u32) }); } } #[test] fn realworld_test() { let bits = usize::BITS as usize; let mut bitvec = BitVec::::with_capacity(LEN * bits); bitvec.resize(LEN * bits, false); assert_eq!(bitvec.len(), LEN * bits); assert_eq!(bitvec.count_ones(), 0); let arc = Arc::new(( BitMap::::new(), Mutex::new(bitvec.into_boxed_bitslice()), )); (0..(LEN * bits * 2)).into_par_iter().for_each(|_| { let index = loop { match arc.0.allocate() { Some(index) => break index, None => (), } }; assert!(unsafe { arc.0.load(index as u32) }); assert!(!arc.1.lock().get_mut(index).unwrap().replace(true)); sleep(Duration::from_micros(1)); let mut guard = arc.1.lock(); unsafe { arc.0.deallocate(index) }; assert!(guard.get_mut(index).unwrap().replace(false)); }); } } concurrent_arena-0.1.10/src/bucket.rs000064400000000000000000000444441046102023000156570ustar 00000000000000use super::{bitmap::BitMap, Arc, OptionExt, SliceExt}; use core::{array, cell::UnsafeCell, hint::spin_loop, ops::Deref}; use std::sync::atomic::{fence, AtomicU8, Ordering}; const REMOVED_MASK: u8 = 1 << (u8::BITS - 1); const REFCNT_MASK: u8 = !REMOVED_MASK; pub const MAX_REFCNT: u8 = REFCNT_MASK; #[derive(Debug)] struct Entry { counter: AtomicU8, val: UnsafeCell>, } impl Entry { const fn new() -> Self { Self { counter: AtomicU8::new(0), val: UnsafeCell::new(None), } } } impl Drop for Entry { fn drop(&mut self) { // Use `Acquire` here to make sure option is set to None before // the entry is dropped. let cnt = self.counter.load(Ordering::Acquire); // It must be either deleted, or is still alive // but no `ArenaArc` reference exist. debug_assert!(cnt <= 1); let val = self.val.get_mut().take(); if cnt == 0 { debug_assert!(val.is_none()); } else { debug_assert!(val.is_some()); } } } #[derive(Debug)] pub(crate) struct Bucket { bitset: BitMap, entries: [Entry; LEN], } unsafe impl Sync for Bucket { } unsafe impl Send for Bucket { } impl Default for Bucket { fn default() -> Self { Self::new() } } impl Bucket { pub(crate) fn new() -> Self { Self { bitset: BitMap::new(), entries: array::from_fn(|_| Entry::new()), } } pub(crate) fn try_insert( this: &Arc, bucket_index: u32, value: T, ) -> Result, T> { let index = match this.bitset.allocate() { Some(index) => index, None => return Err(value), }; // Safety: index <= LEN let entry = unsafe { this.entries.get_unchecked_on_release(index) }; // Use `Acquire` here to make sure option is set to None before // the entry is reused again. let prev_refcnt = entry.counter.load(Ordering::Acquire); debug_assert_eq!(prev_refcnt, 0); let ptr = entry.val.get(); // Safety: ptr can only accessed by this thread let res = unsafe { ptr.replace(Some(value)) }; debug_assert!(res.is_none()); // 1 for the ArenaArc, another is for the Bucket itself. // // Set counter after option is set to `Some(...)` to avoid // race condition with `remove`. if cfg!(debug_assertions) { let prev_refcnt = entry.counter.swap(2, Ordering::Relaxed); assert_eq!(prev_refcnt, 0); } else { entry.counter.store(2, Ordering::Relaxed); } let index = index as u32; Ok(ArenaArc { slot: bucket_index * (LEN as u32) + index, index, bucket: Arc::clone(this), }) } /// # Safety /// /// `index` <= `LEN` unsafe fn access_impl( this: Arc, bucket_index: u32, index: u32, update_refcnt: fn(u8) -> u8, ) -> Option> { if this.bitset.load(index) { let counter = &this .entries .get_unchecked_on_release(index as usize) .counter; let mut refcnt = counter.load(Ordering::Relaxed); loop { if (refcnt & REMOVED_MASK) != 0 { return None; } if refcnt == 0 { // The variable is not yet fully initialized. // Reload the refcnt and check again. spin_loop(); refcnt = counter.load(Ordering::Relaxed); continue; } match counter.compare_exchange_weak( refcnt, update_refcnt(refcnt), Ordering::Relaxed, Ordering::Relaxed, ) { Ok(_) => break, Err(new_refcnt) => refcnt = new_refcnt, } } Some(ArenaArc { slot: bucket_index * (LEN as u32) + index, index, bucket: this, }) } else { None } } /// # Safety /// /// `index` <= `LEN` pub(crate) unsafe fn get( this: Arc, bucket_index: u32, index: u32, ) -> Option> { Self::access_impl(this, bucket_index, index, |refcnt| refcnt + 1) } /// # Safety /// /// `index` <= `LEN` pub(crate) unsafe fn remove( this: Arc, bucket_index: u32, index: u32, ) -> Option> { Self::access_impl(this, bucket_index, index, |refcnt| refcnt | REMOVED_MASK) } } /// Can have at most `MAX_REFCNT` refcount. #[derive(Debug)] pub struct ArenaArc { slot: u32, index: u32, bucket: Arc>, } impl Unpin for ArenaArc { } impl ArenaArc { pub fn slot(this: &Self) -> u32 { this.slot } fn get_index(this: &Self) -> usize { this.index as usize } fn get_entry(this: &Self) -> &Entry { // Safety: `Self::get_index(this)` <= `LEN` let entry = unsafe { this.bucket .entries .get_unchecked_on_release(Self::get_index(this)) }; debug_assert!((entry.counter.load(Ordering::Relaxed) & REFCNT_MASK) > 0); entry } pub fn strong_count(this: &Self) -> u8 { let entry = Self::get_entry(this); let cnt = entry.counter.load(Ordering::Relaxed) & REFCNT_MASK; debug_assert!(cnt > 0); cnt } pub fn is_removed(this: &Self) -> bool { let counter = &Self::get_entry(this).counter; let refcnt = counter.load(Ordering::Relaxed); (refcnt & REMOVED_MASK) != 0 } /// Remove this element. /// /// Return true if succeeds, false if it is already removed. pub fn remove(this: &Self) -> bool { let counter = &Self::get_entry(this).counter; let mut refcnt = counter.load(Ordering::Relaxed); loop { debug_assert_ne!(refcnt & REFCNT_MASK, 0); if (refcnt & REMOVED_MASK) != 0 { // already removed return false; } // Since the element is not removed, there is at least two ref to it: // - From the bucket itself // - From `self` debug_assert_ne!(refcnt, 1); match counter.compare_exchange_weak( refcnt, // Reduce refcnt by one since it is removed from bucket. (refcnt - 1) | REMOVED_MASK, Ordering::Relaxed, Ordering::Relaxed, ) { Ok(_) => return true, Err(new_refcnt) => refcnt = new_refcnt, } } } } impl Deref for ArenaArc { type Target = T; fn deref(&self) -> &Self::Target { let ptr = Self::get_entry(self).val.get(); // Safety: `Self::get_index(this)` <= `LEN` unsafe { (*ptr).as_ref().unwrap_unchecked_on_release() } } } impl Clone for ArenaArc { fn clone(&self) -> Self { let entry = Self::get_entry(self); // According to [Boost documentation][1], increasing the refcount // can be done using Relaxed operation since there are at least one // reference alive. // // [1]: https://www.boost.org/doc/libs/1_77_0/doc/html/atomic/usage_examples.html if (entry.counter.fetch_add(1, Ordering::Relaxed) & REFCNT_MASK) == MAX_REFCNT { panic!("ArenaArc can have at most u8::MAX refcount"); } Self { slot: self.slot, index: self.index, bucket: Arc::clone(&self.bucket), } } } impl Drop for ArenaArc { fn drop(&mut self) { let entry = Self::get_entry(self); // According to [Boost documentation][1], decreasing refcount must be done // using Release to ensure the write to the value happens before the // reference is dropped. // // [1]: https://www.boost.org/doc/libs/1_77_0/doc/html/atomic/usage_examples.html let prev_counter = entry.counter.fetch_sub(1, Ordering::Release); let prev_refcnt = prev_counter & MAX_REFCNT; debug_assert_ne!(prev_refcnt, 0); if prev_refcnt == 1 { debug_assert_eq!(prev_counter, REMOVED_MASK | 1); // This is the last reference, drop the value. // According to [Boost documentation][1], an Acquire fence must be used // before dropping value to ensure that all write to the value happens // before it is dropped. fence(Ordering::Acquire); // Now entry.counter == 0 // Safety: `entry.val` can only be accessed by this thread now. let option = unsafe { &mut *entry.val.get() }; *option = None; // Make sure drop is written to memory before // the entry is reused again. entry.counter.store(0, Ordering::Release); // Safety: // // `Self::get_index(self)` <= `LEN` == `BITARRAY_LEN / usize::BITS` unsafe { self.bucket.bitset.deallocate(Self::get_index(self)) }; } } } #[cfg(test)] mod tests { use super::Arc; use super::ArenaArc; use parking_lot::Mutex; use parking_lot::MutexGuard; use std::thread::sleep; use std::thread::spawn; use std::time::Duration; use rayon::prelude::*; const LEN: u32 = usize::BITS; type Bucket = super::Bucket; #[test] fn test_basic() { let bucket: Arc> = Arc::new(Bucket::new()); let arcs: Vec<_> = (0..LEN) .into_par_iter() .map(|i| { let arc = Bucket::try_insert(&bucket, 0, i).unwrap(); assert_eq!(ArenaArc::strong_count(&arc), 2); assert_eq!(*arc, i); arc }) .collect(); assert!(Bucket::try_insert(&bucket, 0, 0).is_err()); for (i, each) in arcs.iter().enumerate() { assert_eq!((**each) as usize, i); } let arcs_get: Vec<_> = (&arcs) .into_par_iter() .enumerate() .map(|(i, orig_arc)| { let arc = unsafe { Bucket::get(Arc::clone(&bucket), 0, orig_arc.index) }.unwrap(); assert_eq!(ArenaArc::strong_count(&arc), 3); assert_eq!(*arc as usize, i); arc }) .collect(); for (i, each) in arcs_get.iter().enumerate() { assert_eq!((**each) as usize, i); } } #[test] fn test_clone() { let bucket: Arc> = Arc::new(Bucket::new()); let arcs: Vec<_> = (0..LEN) .into_par_iter() .map(|i| { let arc = Bucket::try_insert(&bucket, 0, i).unwrap(); assert_eq!(ArenaArc::strong_count(&arc), 2); assert_eq!(*arc, i); arc }) .collect(); let arcs_cloned: Vec<_> = arcs .iter() .map(|arc| { let new_arc = arc.clone(); assert_eq!(ArenaArc::strong_count(&new_arc), 3); assert_eq!(ArenaArc::strong_count(arc), 3); new_arc }) .collect(); drop(arcs); drop(bucket); // bucket are dropped, however as long as the arcs // are alive, these values are still kept alive. for (i, each) in arcs_cloned.iter().enumerate() { assert_eq!((**each) as usize, i); } } #[test] fn test_reuse() { let bucket: Arc> = Arc::new(Bucket::new()); let mut arcs: Vec<_> = (0..LEN) .into_par_iter() .map(|i| { let arc = Bucket::try_insert(&bucket, 0, i).unwrap(); assert_eq!(ArenaArc::strong_count(&arc), 2); assert_eq!(*arc, i); arc }) .collect(); for arc in arcs.drain(arcs.len() / 2..) { assert_eq!(ArenaArc::strong_count(&arc), 2); let new_arc = unsafe { Bucket::remove(bucket.clone(), 0, arc.index) }.unwrap(); assert_eq!(ArenaArc::strong_count(&arc), 2); assert!(ArenaArc::is_removed(&new_arc)); drop(new_arc); assert_eq!(ArenaArc::strong_count(&arc), 1); } let new_arcs: Vec<_> = (LEN..LEN + LEN / 2) .into_par_iter() .map(|i| { let arc = Bucket::try_insert(&bucket, 0, i).unwrap(); assert_eq!(ArenaArc::strong_count(&arc), 2); assert_eq!(*arc, i); arc }) .collect(); let handle1 = spawn(move || { arcs.into_par_iter().enumerate().for_each(|(i, each)| { assert_eq!((*each) as usize, i); }); }); let handle2 = spawn(move || { new_arcs .into_par_iter() .zip(LEN..LEN + LEN / 2) .for_each(|(each, i)| { assert_eq!(*each, i); }); }); handle1.join().unwrap(); handle2.join().unwrap(); } #[test] fn test_reuse2() { let bucket: Arc> = Arc::new(Bucket::new()); let mut arcs: Vec<_> = (0..LEN) .into_par_iter() .map(|i| { let arc = Bucket::try_insert(&bucket, 0, i).unwrap(); assert_eq!(ArenaArc::strong_count(&arc), 2); assert_eq!(*arc, i); arc }) .collect(); for arc in arcs.drain(arcs.len() / 2..) { assert_eq!(ArenaArc::strong_count(&arc), 2); ArenaArc::remove(&arc); assert!(ArenaArc::is_removed(&arc)); assert_eq!(ArenaArc::strong_count(&arc), 1); } let new_arcs: Vec<_> = (LEN..LEN + LEN / 2) .into_par_iter() .map(|i| { let arc = Bucket::try_insert(&bucket, 0, i).unwrap(); assert_eq!(ArenaArc::strong_count(&arc), 2); assert_eq!(*arc, i); arc }) .collect(); let handle1 = spawn(move || { arcs.into_par_iter().enumerate().for_each(|(i, each)| { assert_eq!((*each) as usize, i); }); }); let handle2 = spawn(move || { new_arcs .into_par_iter() .zip(LEN..LEN + LEN / 2) .for_each(|(each, i)| { assert_eq!(*each, i); }); }); handle1.join().unwrap(); handle2.join().unwrap(); } #[test] fn test_concurrent_remove() { let bucket: Arc> = Arc::new(Bucket::new()); let arcs: Vec<_> = (0..LEN) .into_par_iter() .map(|i| { let arc = Bucket::try_insert(&bucket, 0, i).unwrap(); assert_eq!(ArenaArc::strong_count(&arc), 2); assert_eq!(*arc, i); arc }) .collect(); arcs.into_par_iter().for_each(|arc| { assert_eq!(ArenaArc::strong_count(&arc), 2); let new_arc = unsafe { Bucket::remove(bucket.clone(), 0, arc.index) }.unwrap(); assert!(ArenaArc::is_removed(&new_arc)); assert_eq!(ArenaArc::strong_count(&arc), 2); drop(new_arc); assert_eq!(ArenaArc::strong_count(&arc), 1); }); } #[test] fn test_concurrent_remove2() { let bucket: Arc> = Arc::new(Bucket::new()); let arcs: Vec<_> = (0..LEN) .into_par_iter() .map(|i| { let arc = Bucket::try_insert(&bucket, 0, i).unwrap(); assert_eq!(ArenaArc::strong_count(&arc), 2); assert_eq!(*arc, i); arc }) .collect(); arcs.into_par_iter().for_each(|arc| { assert_eq!(ArenaArc::strong_count(&arc), 2); ArenaArc::remove(&arc); assert!(ArenaArc::is_removed(&arc)); assert_eq!(ArenaArc::strong_count(&arc), 1); }); } #[test] fn realworld_test() { let bucket: Arc>> = Arc::new(Bucket::new()); (0..LEN).into_par_iter().for_each(|i| { let arc = Bucket::try_insert(&bucket, 0, Mutex::new(i)).unwrap(); assert_eq!(ArenaArc::strong_count(&arc), 2); assert_eq!(*arc.lock(), i); let arc_cloned = arc.clone(); let f = move |mut guard: MutexGuard<'_, u32>| { if *guard == i { *guard = i + 1; } else if *guard == i + 1 { *guard = i + 2; } else { panic!(""); } }; let handle = spawn(move || { sleep(Duration::from_micros(1)); f(arc_cloned.lock()); }); spawn(move || { sleep(Duration::from_micros(1)); f(arc.lock()); handle.join().unwrap(); assert_eq!(*arc.lock(), i + 2); }); }); } } concurrent_arena-0.1.10/src/lib.rs000064400000000000000000000004641046102023000151420ustar 00000000000000mod arcs; mod arena; mod bitmap; mod bucket; mod thread_id; mod utility; use utility::{OptionExt, SliceExt}; pub use arena::Arena; pub use bucket::{ArenaArc, MAX_REFCNT}; /// `triomphe::Arc` does not support weak reference, thus it allocates one `usize` less /// than `std::sync::Arc`. use triomphe::Arc; concurrent_arena-0.1.10/src/thread_id.rs000064400000000000000000000002631046102023000163140ustar 00000000000000use parking_lot::{lock_api::GetThreadId, RawThreadId}; /// Return a non zero thread id pub(crate) fn get_thread_id() -> usize { RawThreadId::INIT.nonzero_thread_id().get() } concurrent_arena-0.1.10/src/utility.rs000064400000000000000000000014701046102023000160750ustar 00000000000000use std::slice::SliceIndex; pub(crate) trait OptionExt { unsafe fn unwrap_unchecked_on_release(self) -> T; } impl OptionExt for Option { unsafe fn unwrap_unchecked_on_release(self) -> T { if cfg!(debug_assertions) { self.unwrap() } else { self.unwrap_unchecked() } } } pub(crate) trait SliceExt { unsafe fn get_unchecked_on_release(&self, index: I) -> &>::Output where I: SliceIndex<[T]>; } impl SliceExt for [T] { unsafe fn get_unchecked_on_release(&self, index: I) -> &>::Output where I: SliceIndex<[T]>, { if cfg!(debug_assertions) { self.get(index).unwrap() } else { self.get_unchecked(index) } } }