mmtk/util/rust_util/atomic_box.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
use std::sync::atomic::{AtomicPtr, Ordering};
use bytemuck::Zeroable;
/// A lazily initialized box. Similar to an `Option<Box<T>>`, but can be initialized atomically.
///
/// It is designed for implementing shared data. Therefore, methods with `&self`, namely
/// [`OnceOptionBox::get`] and the [`OnceOptionBox::get_or_init`] methods, only return shared
/// references to the content (`&T`). The user should use types that support multi-threaded
/// accesses, such as mutexes or atomic types, if the inner type is supposed to be modified
/// concurrently.
///
/// Once initialized, this object will own its content. The content is allocated in the heap, and
/// will be dropped and deallocated when this instance is dropped.
///
/// # Comparison to existing data structures
///
/// [`std::sync::OnceLock`] also provides thread-safe lazily-initialized cells. But as its name
/// suggests, it uses locks for synchronization, whereas `OnceOptionBox` is lock-free. `OnceLock`
/// also has a field of [`std::sync::Once`] which increases the space overhead. `OnceOptionBox`
/// only has one atomic pointer field and is more suitable for large arrays of lazily initialized
/// elements.
pub struct OnceOptionBox<T> {
inner: AtomicPtr<T>,
}
impl<T> OnceOptionBox<T> {
/// Create an empty `OnceOptionBox` instance.
pub fn new() -> OnceOptionBox<T> {
Self {
inner: AtomicPtr::new(std::ptr::null_mut()),
}
}
/// Get a reference to the content of this box, or `None` if not yet initialized.
pub fn get(&self, order: Ordering) -> Option<&T> {
let ptr = self.inner.load(order);
unsafe { ptr.as_ref() }
}
/// Get a reference to the content of this box. If not initialized, it will call `init` to
/// initialize this box.
///
/// When multiple threads attempt to initialize this box concurrently, all threads may call
/// their supplied `init` closure, but only one thread will successfully initialize this box to
/// the return value of `init`. Other threads will drop their return values of `init`. All
/// callers will return the reference to the value created by the successful thread.
pub fn get_or_init(
&self,
order_load: Ordering,
order_store: Ordering,
init: impl FnOnce() -> T,
) -> &T {
if let Some(get_result) = self.get(order_load) {
return get_result;
}
let new_inner = Box::into_raw(Box::new(init()));
let cas_result = self.inner.compare_exchange(
std::ptr::null_mut(),
new_inner,
order_store,
Ordering::Relaxed,
);
match cas_result {
Ok(old_inner) => {
debug_assert_eq!(old_inner, std::ptr::null_mut());
unsafe { new_inner.as_ref().unwrap() }
}
Err(old_inner) => {
drop(unsafe { Box::from_raw(new_inner) });
unsafe { old_inner.as_ref().unwrap() }
}
}
}
}
impl<T> Drop for OnceOptionBox<T> {
fn drop(&mut self) {
let ptr = *self.inner.get_mut();
if !ptr.is_null() {
drop(unsafe { Box::from_raw(ptr) });
}
}
}
unsafe impl<T> Zeroable for OnceOptionBox<T> {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn construct() {
let oob = OnceOptionBox::<usize>::new();
let elem = oob.get(Ordering::Relaxed);
assert_eq!(elem, None);
}
#[test]
fn init() {
let oob = OnceOptionBox::<usize>::new();
let elem = oob.get_or_init(Ordering::Relaxed, Ordering::Relaxed, || 42);
assert_eq!(*elem, 42);
}
#[test]
fn reinit() {
let oob = OnceOptionBox::<usize>::new();
let elem = oob.get_or_init(Ordering::Relaxed, Ordering::Relaxed, || 42);
assert_eq!(*elem, 42);
let elem2 = oob.get_or_init(Ordering::Relaxed, Ordering::Relaxed, || 43);
assert_eq!(*elem2, 42);
}
}