diff --git a/README.md b/README.md new file mode 100644 index 0000000..11d620e --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +A [`LinkedList`] implementation avoiding the use of [`Arc`]s in favor of unsafe manual removal of nodes when the caller knows all possible references are left unused. + +The point of this crate is to offer [`Pin`] guarantees on the references into the list while allowing it to be modified. The implementation of all this doesn't require mutable access to the linked list itself so as a side effect it's possible to use the list in concurrent manners. + +This means that it will try as smartly as possible to allow concurrent modifications to it as long as the nodes affected are unrelated. + +--- + +`cargo doc` is supported and is the main documentation of the library. But there's no official hosting of the document files. diff --git a/src/double/mod.rs b/src/double/mod.rs new file mode 100644 index 0000000..10a9818 --- /dev/null +++ b/src/double/mod.rs @@ -0,0 +1,112 @@ +//! Doubly non-Arc linked list. +//! +//! Doubly as each node points to the next and previous node. + +use std::ops::Deref; + +use parking_lot::RwLock; + +use crate::double::node::{BackNodeWriteLock, Node, NodeBackPtr}; + +pub mod node; + +// # Rules to prevent deadlocks +// +// Left locking must be `try_` and if it fails at any point, the way rightwards must be cleared in +// case the task holding the left lock is moving rightwards. +// Rightwards locking can be blocking. + +pub struct NodeHeadInner<'ll, T> { + start: Option<&'ll node::Node<'ll, T>>, +} + +impl Default for NodeHeadInner<'_, T> { + fn default() -> Self { + Self { start: None } + } +} + +// TODO: +// impl<'ll, T> Drop for LinkedList<'ll, T> { +// fn drop(&mut self) { +// // SAFETY: this is the very last ref of &self so it can pretty much assume no external +// // refs into the inner data as they would be invalid to live after this +// while unsafe { self.pop().is_some() } {} +// } +// } + +pub struct LinkedList<'ll, T>(RwLock>); + +impl Default for LinkedList<'_, T> { + #[must_use] + fn default() -> Self { + Self(RwLock::new(NodeHeadInner::default())) + } +} + +impl<'ll, T> Deref for LinkedList<'ll, T> { + type Target = RwLock>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'ll, T> LinkedList<'ll, T> { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + pub fn prepend(&'ll self, data: T) { + let self_lock = self.write(); + let next = self_lock.start; + let next_lock = next.map(|n| n.write()); + let new_node = Node::new_leaked(data, NodeBackPtr::new_head(self), next); + // SAFETY: ptrs are surrounding and they've been locked all along + unsafe { new_node.integrate((BackNodeWriteLock::Head(self_lock), next_lock)) }; + } + + /// Returns [`None`] if there's no next node. + /// + /// # Safety + /// + /// There must be no outer references to the first node. + pub unsafe fn pop(&'ll self) -> Option<()> { + let self_lock = self.write(); + let pop_node = self_lock.start?; + let pop_node_lock = pop_node.write(); + let next_node_lock = pop_node_lock.next.map(|n| n.write()); + + // SAFETY: locked all along and consecutive nodes + unsafe { + Node::isolate( + &pop_node_lock, + (BackNodeWriteLock::Head(self_lock), next_node_lock), + ); + } + + drop(pop_node_lock); + // SAFETY: node has been isolated so no references out + unsafe { pop_node.wait_free() } + + Some(()) + } + + pub fn clone_into_vec(&self) -> Vec + where + T: Clone, + { + let mut total = Vec::new(); + let mut next_node = self.read().start; + while let Some(node) = next_node { + let read = node.read(); + total.push(read.data.clone()); + next_node = read.next; + } + total + } +} + +#[cfg(test)] +mod tests; diff --git a/src/double/node.rs b/src/double/node.rs new file mode 100644 index 0000000..b06be14 --- /dev/null +++ b/src/double/node.rs @@ -0,0 +1,314 @@ +use std::{ops::Deref, ptr}; + +use parking_lot::{RwLock, RwLockReadGuard, RwLockUpgradableReadGuard, RwLockWriteGuard}; + +use super::NodeHeadInner; + +#[repr(transparent)] +pub struct Node<'ll, T>(RwLock>); +type NodeHead<'ll, T> = RwLock>; +pub enum NodeBackPtr<'ll, T> { + Head(&'ll NodeHead<'ll, T>), + Node(&'ll Node<'ll, T>), +} + +// yes the whole purpose is docs, might add Isolated Guards around nodes here +pub mod topology_safety; + +// TODO: RwLock the ptrs only instead of the node +// Box<(RwLock<(&prev, &next)>, T)> +// instead of +// Box> +// allows user to opt out of RwLock, allowing changes to adyacent nodes while T is being externally +// used and enables T: ?Sized +pub struct NodeInner<'ll, T> { + pub(crate) prev: NodeBackPtr<'ll, T>, + pub(crate) next: Option<&'ll Node<'ll, T>>, + pub data: T, +} + +impl<'ll, T> Deref for Node<'ll, T> { + type Target = RwLock>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Deref for NodeInner<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl Copy for NodeBackPtr<'_, T> {} +impl Clone for NodeBackPtr<'_, T> { + // # TODO: check if this works as expected with the cacnonical clone impl as I'm not sure if + // Copy would make it recursive or not + #[allow(clippy::enum_glob_use, clippy::non_canonical_clone_impl)] + fn clone(&self) -> Self { + use NodeBackPtr::*; + + match self { + Head(h) => Head(h), + Node(n) => Node(n), + } + } +} +type WriteAndBackDoublet<'ll, T> = ( + BackNodeWriteLock<'ll, T>, + RwLockUpgradableReadGuard<'ll, NodeInner<'ll, T>>, +); +type WriteSurroundTriplet<'ll, T> = ( + BackNodeWriteLock<'ll, T>, + RwLockWriteGuard<'ll, NodeInner<'ll, T>>, + Option>>, +); +type WriteOnlyAroundTriplet<'ll, T> = ( + BackNodeWriteLock<'ll, T>, + Option>>, +); +impl<'ll, T> Node<'ll, T> { + // TODO: think about the isolaed state of the following 3 fn's + + /// Creates a new node in the heap, will link to `prev` and `next` but will be isolated, it can + /// be thought of just ita data and the two pointers, having it isolated doesn't guarantee any + /// integration into the linked list. + /// + /// As long as this node exists and is not properly integrated into a linked list, it's + /// considered that the `prev` and `next` refs are being held. + #[must_use] + pub fn new(data: T, prev: NodeBackPtr<'ll, T>, next: Option<&'ll Node<'ll, T>>) -> Self { + Self(RwLock::new(NodeInner { prev, next, data })) + } + + /// Boxes [`self`] + #[must_use] + pub fn boxed(self) -> Box { + Box::new(self) + } + + /// Leaks [`self`] as a [`Box`] + #[must_use] + pub fn leak(self: Box) -> &'static mut Self { + Box::leak(self) + } + + pub fn new_leaked( + data: T, + prev: NodeBackPtr<'ll, T>, + next: Option<&'ll Node<'ll, T>>, + ) -> &'ll mut Self { + Box::leak(Box::new(Self::new(data, prev, next))) + } + + /// # Safety + /// + /// The [`self`] pointer must come from a [`Box`] allocation like [`Self::boxed`] and + /// [`Self::leak`]. + pub unsafe fn free(self: *mut Self) { + drop(unsafe { Box::from_raw(self) }); + } + + /// Frees the current node but waits until the inner [`RwLock`] has no waiters. + /// + /// # Safety + /// + /// There must be no references left to [`self`] + pub unsafe fn wait_free(&self) { + loop { + if self.is_locked() { + drop(self.write()); + } else { + break; + } + } + + let myself = ptr::from_ref(self).cast_mut(); + unsafe { myself.free() } + } + + pub fn lock_and_back(&'ll self) -> WriteAndBackDoublet<'ll, T> { + let mut self_read = self.upgradable_read(); + // "soft" back lock + match self_read.prev.try_write() { + Some(prev_write) => (prev_write, self_read), + None => { + // already locked, no worries but we have to clear for the lock before use its + // possible way forward, we can also wait until `prev` is accesible either case + // (the task holding it could modify us or if it doesn't we need to lock that same + // node) + loop { + let old_prev = self_read.prev; + let old_prev_write = + RwLockUpgradableReadGuard::unlocked_fair(&mut self_read, move || { + old_prev.write() + }); + // we reaquire ourselves after `unlocked_fair` so `self_read` couls have + // changed + if NodeBackPtr::ptr_eq(&self_read.prev, &old_prev) { + break (old_prev_write, self_read); + } + } + } + } + } + + /// Attempts to get a write lock on the surrouding nodes + pub fn write_surround(&'ll self) -> WriteSurroundTriplet<'ll, T> { + // backward blocking must be try + let (prev_write, self_read) = self.lock_and_back(); + // Now `prev` is write locked and we can block forwards + let self_write = RwLockUpgradableReadGuard::upgrade(self_read); + let next_write = self_write.next.map(|n| n.write()); + + (prev_write, self_write, next_write) + } + + /// # Safety + /// + /// The passed locks must also be consecutive for this to respect topology. + /// + /// This node will remain isolated. See [`topology_safety`]. + pub unsafe fn isolate(self_read: &NodeInner<'ll, T>, locks: WriteOnlyAroundTriplet<'ll, T>) { + let (mut back_write, next_write) = locks; + + back_write.set_next(self_read.next); + if let Some(mut next_write) = next_write { + next_write.prev = self_read.prev; + } + } + + /// # Safety + /// + /// The passed locks must be surrounding for this to respect topology. + /// + /// This taken node ([`self`]) must be an isolated node. See [`topology_safety`]. + pub unsafe fn integrate(&'ll self, locks: WriteOnlyAroundTriplet<'ll, T>) { + let (mut back_write, next_write) = locks; + + back_write.set_next(Some(self)); + if let Some(mut next_write) = next_write { + next_write.prev = NodeBackPtr::new_node(self); + } + } + + /// # Safety + /// + /// [`self`] must be integrated into the linked list. See [`topology_safety`]. + /// + /// Assumes there's no other external references into this node when called as it will be + /// deallocated. This will also wait for all waiters into the node lock to finish before really + /// freeing it, this includes concurrent calls to this same node. + pub unsafe fn remove(&'ll self) { + let surround_locks = self.write_surround(); + let (prev, myself, next) = surround_locks; + let around_locks = (prev, next); + // Should be integrated and the surrounding locks are consesutive and locked all along + unsafe { Self::isolate(&myself, around_locks) } + + // lazy-wait for no readers remaining + drop(myself); + // SAFETY: The node is isolated so good to be freed. + unsafe { self.wait_free() } + } +} + +/// Generic Write Lock of a [`NodeBackPtr`] +pub enum BackNodeWriteLock<'ll, T> { + Head(RwLockWriteGuard<'ll, NodeHeadInner<'ll, T>>), + Node(RwLockWriteGuard<'ll, NodeInner<'ll, T>>), +} + +/// Generic Read Lock of a [`NodeBackPtr`] +pub enum BackNodeReadLock<'ll, T> { + Head(RwLockReadGuard<'ll, NodeHeadInner<'ll, T>>), + Node(RwLockReadGuard<'ll, NodeInner<'ll, T>>), +} + +#[allow(clippy::enum_glob_use)] +impl<'ll, T> NodeBackPtr<'ll, T> { + #[must_use] + pub fn ptr_eq(&self, other: &Self) -> bool { + use NodeBackPtr::*; + + match (self, other) { + (Head(h1), Head(h2)) => ptr::eq(h1, h2), + (Node(n1), Node(n2)) => ptr::eq(n1, n2), + _ => false, + } + } + + #[must_use] + pub fn new_node(node: &'ll Node<'ll, T>) -> Self { + Self::Node(node) + } + #[must_use] + pub fn new_head(head: &'ll NodeHead<'ll, T>) -> Self { + Self::Head(head) + } + + /// Analogous to [`RwLock::write`] + #[must_use] + pub fn write(&self) -> BackNodeWriteLock<'ll, T> { + use BackNodeWriteLock as WL; + use NodeBackPtr::*; + + match self { + Head(h) => WL::Head(h.write()), + Node(n) => WL::Node(n.write()), + } + } + + /// Analogous to [`RwLock::read`] + #[must_use] + pub fn read(&self) -> BackNodeReadLock<'ll, T> { + use BackNodeReadLock as RL; + use NodeBackPtr::*; + + match self { + Head(h) => RL::Head(h.read()), + Node(n) => RL::Node(n.read()), + } + } + + /// Analogous to [`RwLock::try_write`] + #[must_use] + pub fn try_write(&self) -> Option> { + use BackNodeWriteLock as WL; + use NodeBackPtr::*; + + Some(match self { + Head(h) => WL::Head(h.try_write()?), + Node(n) => WL::Node(n.try_write()?), + }) + } + + /// Analogous to [`RwLock::try_read`] + #[must_use] + pub fn try_read(&self) -> Option> { + use BackNodeReadLock as RL; + use NodeBackPtr::*; + + Some(match self { + Head(h) => RL::Head(h.try_read()?), + Node(n) => RL::Node(n.try_read()?), + }) + } +} + +impl<'ll, T> BackNodeWriteLock<'ll, T> { + #[allow(clippy::enum_glob_use)] + fn set_next(&mut self, next: Option<&'ll Node<'ll, T>>) { + use BackNodeWriteLock::*; + + match self { + Head(h) => h.start = next, + Node(n) => n.next = next, + } + } +} + +impl NodeInner<'_, T> {} diff --git a/src/double/node/topology_safety.rs b/src/double/node/topology_safety.rs new file mode 100644 index 0000000..4b704e3 --- /dev/null +++ b/src/double/node/topology_safety.rs @@ -0,0 +1,71 @@ +//! The linked list is supposed to have bilinear continuity (can be iterated forwards and +//! backwards for each node and it will be linear). +//! +//! "Topology Safety" is the term I made for functions that if used incorrectly can break this +//! topology. +//! +//! # Side Effects +//! +//! Side effects to breaking the topology could be: +//! * Leaving unrelated nodes out of the linearity of the node. +//! * Creating loops in the linked list. +//! +//! I'm not so sure of these two but it depends on how hard you break the topology. +//! +//! It's completely fine although not recommended to unsafely manipulate the topology of the +//! linked list if you know what you are doing. +//! +//! However, [`LinkedList`]'s [`Drop`] implementation assumes the list can be iterated forward +//! and will attempt to drop each element it finds. If you broke the topology where this is +//! undoable you might want to use [`ManuallyDrop`] on it. +//! +//! # Safety +//! +//! For these functions to be topology safe, if they take locks to nodes, you must make sure +//! any related nodes and adyacent ones (as they hold pointers into that section) are locked +//! for all the operation's duration. +//! +//! If any node to be integrated had the adyacent nodes locked since its creation, it would be +//! safe to integrate in. +//! +//! # Examples +//! +//! Assume the following [`LinkedList`]: +//! ```txt +//! A -> B +//! ``` +//! +//! If you then create `C` and `D` isolated after `A`. The [`LinkedList`] would remain the same +//! (as they are isolated) but `C` and `D` would have broken topology views of the list. +//! +//! If you then were to integrate `C` and create another `E` isolated after the newly +//! integrated `C`. +//! ```txt +//! A -> C -> B +//! // But D and E would thing +//! D: A -> D -> B +//! E: A -> C -> E -> B +//! ``` +//! +//! If you now integrate `D`, `C` would be isolated indirectly without it knowing. But `E` is +//! also unaware of this and thinks `C` is integrated. So finally integrating `E` would lead +//! to: +//! ```txt +//! ╭─> A -> D -> B <╮ +//! │ C <- E <──╯ │ +//! ╰───┴────────────╯ +//! ``` +//! +//! No reading the chain from different points of view leads to: +//! ```txt +//! [A] -> D -> B +//! A -> C -> E -> [B] +//! A -> [C] -> E -> B +//! A -> [D] -> B +//! A -> C -> [E] -> B +//! ``` +//! +//! This is more a graph than a linked list. + +#[allow(unused_imports)] +use {super::NodeHeadInner, std::mem::ManuallyDrop}; diff --git a/src/double/tests.rs b/src/double/tests.rs new file mode 100644 index 0000000..3b9d170 --- /dev/null +++ b/src/double/tests.rs @@ -0,0 +1,81 @@ +#![allow(clippy::items_after_statements)] + +use std::{ + fmt::Debug, + sync::{ + Barrier, + atomic::{AtomicUsize, Ordering}, + }, + thread, +}; + +use super::*; + +static DROP_C: AtomicUsize = AtomicUsize::new(0); + +#[derive(Clone)] +struct StringWithDrop(String); + +impl From for StringWithDrop { + fn from(value: String) -> Self { + Self(value) + } +} + +impl Drop for StringWithDrop { + fn drop(&mut self) { + DROP_C.fetch_add(1, Ordering::Relaxed); + } +} + +impl Debug for StringWithDrop { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + ::fmt(&self.0, f) + } +} + +#[test] +fn it_works() { + const CREATE1: usize = 100; + const CREATE2: usize = 100; + const NOT_POP: usize = 20; + + // TODO: Find out WHYTF do I need to scope this and compiler won't let me `drop` + { + let ll = LinkedList::::new(); + let barrier = Barrier::new(3); + + thread::scope(|s| { + s.spawn(|| { + barrier.wait(); + + for n in 0..CREATE1 { + ll.prepend(format!("A {n}").into()); + } + }); + s.spawn(|| { + barrier.wait(); + + for n in 0..CREATE2 { + ll.prepend(format!("B {n}").into()); + } + }); + s.spawn(|| { + barrier.wait(); + + for _ in 0..(CREATE1 + CREATE2 - NOT_POP) { + unsafe { + while ll.pop().is_none() { + std::thread::yield_now(); + } + } + } + }); + }); + + assert_eq!(DROP_C.load(Ordering::Relaxed), CREATE1 + CREATE2 - NOT_POP); + } + + // TODO: when drop is impl + // assert_eq!(DROP_C.load(Ordering::Relaxed), CREATE1 + CREATE2); +} diff --git a/src/lib.rs b/src/lib.rs index c928c76..088a120 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,399 +1,10 @@ -#![feature(arbitrary_self_types, offset_of_enum, generic_const_items)] +#![doc = include_str!("../README.md")] +#![feature(arbitrary_self_types, arbitrary_self_types_pointers)] #![warn(clippy::pedantic)] -#![allow(incomplete_features)] -use std::{ - hint::unreachable_unchecked, - mem::{MaybeUninit, offset_of}, - ops::Deref, -}; +#[cfg(doc)] +use std::{pin::Pin, sync::Arc}; -use parking_lot::{RwLock, RwLockWriteGuard}; +pub mod double; -mod docs { - //! Rules for soundness of modifications. - //! To modify the pointer that goes to a node a write lock must be held to it: e.g: - //! - Bidirectional consistency is not guaranteed. If you "walk" a list you must only do so in - //! the same direction, that continuity will be guaranteed. - //! - If N node is to be removed, write lock it. Update adyacent pointers first and keep them - //! locked until the N node is freed, then release the adyacenty locks properly. - //! - The previous prevents deadlocks because by having a write lock of the previous node - //! before locking itself it guaratees that the previous lock can't get read access to - //! itself to get the ptr to the node of ourselves and update our prev ptr. - //! - For every operation only a single item in the list must be write blocked to prevent - //! deadlocks. -} - -pub type NodeHead = LinkedList; -pub enum NodeDiscr { - Head(RwLock>), - Node(Node), -} - -impl Default for NodeDiscr { - fn default() -> Self { - Self::Head(RwLock::new(LinkedList::default())) - } -} - -impl NodeDiscr { - #[must_use] - pub fn new(value: LinkedList) -> Self { - Self::Head(RwLock::new(value)) - } - - /// # Safety - /// UB if [`self`] is not [`Self::Head`]. - pub unsafe fn as_head_unchecked(&self) -> &RwLock> { - let Self::Head(head) = self else { - unsafe { unreachable_unchecked() } - }; - head - } - - /// # Safety - /// UB if [`self`] is not [`Self::Node`]. - pub unsafe fn as_node_unchecked(&self) -> &Node { - let Self::Node(node) = self else { - unsafe { unreachable_unchecked() } - }; - node - } - - fn try_write(&'static self) -> Option> { - match self { - NodeDiscr::Head(h) => { - let lock = h.try_write()?; - Some(NodeDiscrWriteLocks::Head(lock)) - } - NodeDiscr::Node(n) => { - let lock = n.0.try_write()?; - Some(NodeDiscrWriteLocks::Node(lock)) - } - } - } - - /// # Safety - /// - /// Will leak if not handled properly. - #[must_use] - #[allow(clippy::mut_from_ref)] - fn alloc_new_with_ptrs( - data: T, - next: Option<&'static Node>, - prev: &'static NodeDiscr, - ) -> (&'static Self, &'static Node) { - let discr = Box::leak(Box::new(NodeDiscr::Node(Node(RwLock::new(NodeInner { - next: MaybeUninit::new(next), - prev: MaybeUninit::new(prev), - isolated: false, - data, - }))))); - (discr, unsafe { discr.as_node_unchecked() }) - } -} - -#[allow(dead_code)] // We dont even read variants, just hold whatever lock -enum NodeDiscrWriteLocks<'a, T: 'static> { - Head(RwLockWriteGuard<'a, NodeHead>), - Node(RwLockWriteGuard<'a, NodeInner>), -} - -impl NodeDiscrWriteLocks<'_, T> { - fn set_next(&mut self, next: Option<&'static Node>) { - match self { - Self::Head(h) => h.start = next, - Self::Node(n) => n.next = MaybeUninit::new(next), - } - } -} - -#[repr(transparent)] -pub struct Node(RwLock>); - -/// It's safe to assume `next` and `prev` are initialized. But any function which would break this -/// assumption should be considered unsafe. -struct NodeInner { - next: MaybeUninit>>, - prev: MaybeUninit<&'static NodeDiscr>, - /// intended for removal, when the `RwLock` is being "drained" from waiters, there might be - /// another remover waiting, if it finds this it simply aborts - isolated: bool, - data: T, -} - -impl Deref for NodeInner { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.data - } -} - -impl NodeInner { - fn prev(&self) -> &'static NodeDiscr { - unsafe { self.prev.assume_init() } - } - - // /// Could also leak memory - // /// - // /// # Safety - // /// The `prev` self ptr is valid as long as the write lock is held, as soon as it's dropped it - // /// becomes invalid. - // fn try_update_prev(&self) -> Option> { - // match self.prev() { - // NodeDiscr::Head(h) => { - // let mut lock = h.try_write()?; - // lock.start = unsafe { self.next.assume_init() }; - // Some(NodeDiscrWriteLocks::Head(lock)) - // } - // NodeDiscr::Node(n) => { - // let mut lock = n.0.try_write()?; - // lock.next = self.next; - // Some(NodeDiscrWriteLocks::Node(lock)) - // } - // } - // } - - fn next(&self) -> Option<&'static Node> { - unsafe { self.next.assume_init() } - } - - // /// Could also leak memory. - // /// - // /// First option is if theres any next, second one if it locked or not. - // /// - // /// # Safety - // /// The `next` self ptr is valid as long as the write lock is held, as soon as it's dropped it - // /// becomes invalid. - // fn try_update_next(&self) -> Option>>> { - // self.next().map(|next| { - // if let Some(mut lock) = next.0.try_write() { - // lock.prev = self.prev; - // Some(lock) - // } else { - // None - // } - // }) - // } - - #[allow(clippy::type_complexity)] - fn try_write_sides( - &self, - ) -> Option<( - NodeDiscrWriteLocks<'static, T>, - Option>>, - )> { - let prev_lock = self.prev().try_write()?; - let next_lock = if let Some(next_lock) = self.next() { - Some(next_lock.0.try_write()?) - } else { - None - }; - - Some((prev_lock, next_lock)) - } -} - -impl Node { - /// # Safety - /// - /// Node is uninitialized. - /// - /// Will leak if not handled properly. - #[must_use] - #[allow(dead_code)] - unsafe fn alloc_new(data: T) -> &'static mut Self { - Box::leak(Box::new(Node(RwLock::new(NodeInner { - next: MaybeUninit::uninit(), - prev: MaybeUninit::uninit(), - isolated: true, - data, - })))) - } - - /// Isolates the node from surrounding ones and returns a `ReadGuard` to the dangling node that - /// would leak unless freed or managed. This guard could still have readers or writers - /// awaiting. Adyacent write locks are also sent back to prevent their modification since the - /// isolation and make the pointers of self still valid. - /// - /// If it returns None, the node was already isolated. - /// - /// # Safety - /// - /// Its unsafe to access `next` and `prev` ptr's after the edge locks are dropped. - #[allow(clippy::type_complexity)] - fn isolate( - &'_ self, - ) -> Option<( - RwLockWriteGuard<'_, NodeInner>, - ( - NodeDiscrWriteLocks<'static, T>, - Option>>, - ), - )> { - loop { - let mut node = self.0.write(); - if node.isolated { - break None; - } - let Some(mut sides) = node.try_write_sides() else { - drop(node); - std::thread::yield_now(); - continue; - }; - - node.isolated = true; - sides.0.set_next(node.next()); - if let Some(next) = &mut sides.1 { - next.prev = node.prev; - } - break Some((node, sides)); - } - } - - /// # Safety - /// - /// Will remove this pointer from memory, there must be no external pointers to this as they - /// will point to invalid data and UB. - /// - /// Will busy wait for no read/write locks to this slot and assume it's been completely - /// isolated then. Any access attempts while it's being freed (after waiting for locks) can - /// lead to weird UB. - pub unsafe fn remove(&self) { - unsafe { - let Some((node, edge_locks)) = self.isolate() else { - return; - }; - - // Drop the allocated data, edge ptrs remain valid meanwhile - drop(node); // let other readers/writers finish with this item - loop { - if self.0.is_locked() { - std::thread::yield_now(); - } else { - break; - } - } - - // Now that we are the only ref to ourselves its ok to take outselves as mutable - let myself = std::ptr::from_ref(self).cast_mut(); - #[allow(clippy::items_after_statements)] - const OFFSET: usize = offset_of!(NodeDiscr, Node.0); - let myself_discr = myself.wrapping_byte_sub(OFFSET::).cast::>(); - - drop(Box::from_raw(myself_discr)); - drop(edge_locks); // edge ptrs become invalid form now on - } - } -} - -pub struct LinkedList { - start: Option<&'static Node>, -} - -impl Default for LinkedList { - fn default() -> Self { - Self::new() - } -} - -impl LinkedList { - #[must_use] - pub fn new() -> Self { - Self { start: None } - } - - /// # Safety - /// - /// `head_ref` MUST be the [`NodeDiscr`] wrapped around the [`RwLock`] that `self` is locked - /// by. This is asserted in debug mode. - unsafe fn prepend( - mut self: RwLockWriteGuard<'_, Self>, - head_ref: &'static NodeDiscr, - data: T, - ) { - #[cfg(debug_assertions)] - { - let NodeDiscr::Head(ll_head) = head_ref else { - panic!("passed head_ref doesnt match lock"); - }; - debug_assert!(std::ptr::eq(RwLockWriteGuard::rwlock(&self), ll_head)); - } - - let next = self.start; - let (new_node_discr, new_node) = NodeDiscr::alloc_new_with_ptrs(data, next, head_ref); - if let Some(next) = next { - let mut next = next.0.write(); - next.prev = MaybeUninit::new(new_node_discr); - } - self.start = Some(new_node); - } -} - -pub struct LinkedListWrapper { - // Safety: MUST be of the `Head` variant at all moments - inner: NodeDiscr, -} - -impl Default for LinkedListWrapper { - fn default() -> Self { - Self::new() - } -} - -impl LinkedListWrapper { - #[must_use] - pub fn new() -> Self { - Self { - inner: NodeDiscr::default(), - } - } - - pub fn as_head(&'static self) -> &'static RwLock> { - unsafe { self.inner.as_head_unchecked() } - } - - /// # Safety - /// - /// Nothing external must point to the item about to be popped. - pub unsafe fn pop(&'static self) { - loop { - let head_read = self.as_head().read(); - let Some(node) = head_read.start else { - std::thread::yield_now(); - continue; - }; - - unsafe { - drop(head_read); - node.remove(); - break; - } - } - } - - pub fn prepend(&'static self, data: T) { - let lock = self.as_head().write(); - unsafe { - LinkedList::prepend(lock, &self.inner, data); - } - } - - pub fn clone_into_vec(&'static self) -> Vec - where - T: Clone, - { - let mut total = Vec::new(); - let mut next_node = self.as_head().read().start; - while let Some(node) = next_node { - let read = node.0.read(); - total.push(read.data.clone()); - next_node = read.next(); - } - total - } -} - -#[cfg(test)] -mod tests; +pub use double::NodeHeadInner as DoublyLinkedList; diff --git a/src/tests.rs b/src/tests.rs deleted file mode 100644 index b6bf7fd..0000000 --- a/src/tests.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::{ - fmt::Debug, - sync::{ - Barrier, - atomic::{AtomicUsize, Ordering}, - }, - thread, -}; - -use super::*; - -static DROP_C: AtomicUsize = AtomicUsize::new(0); - -#[derive(Clone)] -#[repr(transparent)] -struct StringWithDrop(String); - -impl From for StringWithDrop { - fn from(value: String) -> Self { - Self(value) - } -} - -impl Drop for StringWithDrop { - fn drop(&mut self) { - DROP_C.fetch_add(1, Ordering::Relaxed); - println!("drop {self:?}"); - } -} - -impl Debug for StringWithDrop { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - ::fmt(&self.0, f) - } -} - -#[test] -fn it_works() { - let ll = Box::leak(Box::new(LinkedListWrapper::::new())); - let barrier = Barrier::new(3); - - println!("{:#?}", ll.clone_into_vec()); - - thread::scope(|s| { - s.spawn(|| { - barrier.wait(); - - for n in 0..100 { - ll.prepend(format!("A {n}").into()); - } - }); - s.spawn(|| { - barrier.wait(); - - for n in 0..100 { - ll.prepend(format!("B {n}").into()); - } - }); - s.spawn(|| { - barrier.wait(); - - for _ in 0..180 { - unsafe { - ll.pop(); - } - } - }); - }); - - let a = ll.clone_into_vec(); - println!( - "{:?} len {} dropped {}", - a, - a.len(), - DROP_C.load(Ordering::Relaxed) - ); - - assert_eq!(4, 4); -}