#hazard-pointers #garbage-collection #data-structures #reclamation #shared-ptr #shared-data

no-std haphazard

Dynamic memory management for lock-free data structures using hazard pointers

10 releases

0.1.8 Dec 17, 2023
0.1.7 Oct 3, 2023
0.1.5 Jan 14, 2023
0.1.4 Apr 10, 2022
0.0.0 Jan 30, 2022

#84 in Memory management


Used in 3 crates

Apache-2.0

96KB
1K SLoC

Crates.io Documentation codecov Dependency status

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p1121r3.pdf https://github.com/facebook/folly/tree/0e92d3c2705a45ba7850708fd7fe0c709d6a0e5f

License

Licensed under Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0).

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions.


lib.rs:

Dynamic memory management for lock-free data structures.

This library implements the hazard pointer memory reclamation mechanism, specifically as proposed for the C++ Concurrency Technical Specification. It is adapted from the implementation found in Facebook's Folly library. The initial phases of implementation were all live streamed.

At a high level, hazard pointers provide a mechanism that allows readers of shared pointers to prevent concurrent reclamation of the pointed-to objects by concurrent writers for as long as the read operation is ongoing. When a writer removes an object from a data structure, it instructs the hazard pointer library that said object is no longer reachable (that it is retired), and that the library should eventually drop said object (reclaim it) once it is safe to do so. Readers, meanwhile, inform the library any time they wish to read through a pointer shared with writers. Internally, the library notes down the address that was read in such a way that it can ensure that if the pointed-to object is retired while the reader still has access to it, it is not reclaimed. Only once the reader no longer has access to the read pointer does the library allow the object to be reclaimed.

TODO: Can also help with the ABA problem (ensure object isn't reused until there are no pointers to it, so cannot "see" A again until there are no As left).

For an overview of concurrent garbage collection with hazard pointers, see "Fearless concurrency with hazard pointers". Aaron Turon post on "Lock-freedom without garbage collection" which discusses the alternate approach of using epoch-based reclamation (see below) is also a good reference.

High-level API structure

TODO: Ref section 3 of the proposal and folly's docs.

Hazard pointers vs. other deferred reclamation mechanisms

TODO: Ref sections 3.4 and 4 of the proposal and section from folly's docs.

Note esp. memory usage.

Examples

TODO: Ref section 5 of the proposal and example from folly's docs.

use haphazard::{AtomicPtr, Domain, HazardPointer};

// First, create something that's intended to be concurrently accessed.
let x = AtomicPtr::from(Box::new(42));

// All reads must happen through a hazard pointer, so make one of those:
let mut h = HazardPointer::new();

// We can now use the hazard pointer to read from the pointer without
// worrying about it being deallocated while we read.
let my_x = x.safe_load(&mut h).expect("not null");
assert_eq!(*my_x, 42);

// We can willingly give up the guard to allow writers to reclaim the Box.
h.reset_protection();
// Doing so invalidates the reference we got from .load:
// let _ = *my_x; // won't compile

// Hazard pointers can be re-used across multiple reads.
let my_x = x.safe_load(&mut h).expect("not null");
assert_eq!(*my_x, 42);

// Dropping the hazard pointer releases our guard on the Box.
drop(h);
// And it also invalidates the reference we got from .load:
// let _ = *my_x; // won't compile

// Multiple readers can access a value at once:

let mut h = HazardPointer::new();
let my_x = x.safe_load(&mut h).expect("not null");

let mut h_tmp = HazardPointer::new();
let _ = x.safe_load(&mut h_tmp).expect("not null");
drop(h_tmp);

// Writers can replace the value, but doing so won't reclaim the old Box.
let old = x.swap(Box::new(9001)).expect("not null");

// New readers will see the new value:
let mut h2 = HazardPointer::new();
let my_x2 = x.safe_load(&mut h2).expect("not null");
assert_eq!(*my_x2, 9001);

// And old readers can still access the old value:
assert_eq!(*my_x, 42);

// The writer can retire the value old readers are seeing.
//
// Safety: this value has not been retired before.
unsafe { old.retire() };

// Reads will continue to work fine, as they're guarded by the hazard.
assert_eq!(*my_x, 42);

// Even if the writer actively tries to reclaim retired objects, the hazard makes readers safe.
let n = Domain::global().eager_reclaim();
assert_eq!(n, 0);
assert_eq!(*my_x, 42);

// Only once the last hazard guarding the old value goes away will the value be reclaimed.
drop(h);
let n = Domain::global().eager_reclaim();
assert_eq!(n, 1);

// Remember to also retire the item stored in the AtomicPtr when it's dropped
// (assuming of course that the pointer is not shared elsewhere):
unsafe { x.retire(); }

Differences from the specification

Differences from the folly

TODO: Note differences from spec and from folly. Among other things, see this note from folly.

Dependencies

~0–24MB
~334K SLoC