• Stars
    star
    299
  • Rank 134,604 (Top 3 %)
  • Language
    Rust
  • License
    MIT License
  • Created over 2 years ago
  • Updated about 2 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Fast, efficient, and robust memory reclamation for Rust.

Seize

Crate Github Docs

Fast, efficient, and robust memory reclamation for concurrent data structures.

Introduction

Concurrent data structures are faced with the problem of deciding when it is safe to free memory. Although an object might have been logically removed, other threads that previously loaded it may still be accessing it, and thus it is not safe to free immediately. Over the years, many algorithms have been devised to solve this problem. However, most traditional memory reclamation schemes make the tradeoff between performance, efficiency, and robustness. For example, epoch based reclamation is fast and lightweight but lacks robustness in that a stalled thread can prevent the reclamation of all retired objects. Hazard pointers, another popular scheme, tracks individual pointers, making it efficient and robust but generally much slower.

Another problem that is often not considered is workload balancing. In most reclamation schemes, the thread that retires an object is the one that reclaims it. This leads to unbalanced reclamation in read-dominated workloads; parallelism is degraded when only a fraction of threads are writing. This is especially prevalent with the use of M:N threading models as provided by asynchronous runtimes like Tokio.

Details

Seize is based on the hyaline reclamation scheme, which uses reference counting to determine when it is safe to free memory. However, reference counters are only used for objects that have been retired, allowing it to avoid the high overhead incurred by traditional reference counting schemes where every memory access requires modifying shared memory. Performance is competitive with that of epoch based schemes, while memory efficiency is similar to hazard pointers. Reclamation is naturally balanced as the thread with the last reference to an object is the one that frees it. Epochs can also be optionally tracked to protect against stalled threads, making reclamation truly lock-free.

Seize is compatible with all modern hardware that supports single-word atomic operations such as FAA and CAS.

Guide

Seize tries to stay out of your way as much as possible. It works with raw pointers directly instead of creating safe wrapper types that end up being a hassle to work with in practice. Below is a step-by-step guide on how to get started.

Collectors

Seize avoids the use of global state and encourages creating a designated collector per data structure. Collectors allow you to allocate, protect, and retire objects:

use seize::Collector;

struct Stack<T> {
    collector: Collector,
    // ...
}

impl<T> Stack<T> {
    pub fn new() -> Self {
        Self {
            collector: Collector::new(),
        }
    }
}

Allocating Objects

Seize requires storing some metadata about the global epoch for each object that is allocated. It also needs to reserve a couple words for retirement lists. Because of this, objects in a concurrent data structure that may be reclaimed must take the form of seize::Linked<T>, as opposed to just T.

You can link a value to a collector with the link method. link_boxed is also provided as a quick way to link an object to a collector, and allocate it with Box:

use seize::{reclaim, Collector, Linked};
use std::mem::ManuallyDrop;
use std::sync::atomic::{AtomicPtr, Ordering};

pub struct Stack<T> {
    head: AtomicPtr<Linked<Node<T>>>, // <===
    collector: Collector,
}

struct Node<T> {
    next: *mut Linked<Node<T>>, // <===
    value: ManuallyDrop<T>,
}

impl<T> Stack<T> {
    pub fn push(&self, value: T) {
        let node = self.collector.link_boxed(Node { // <===
            next: std::ptr::null_mut(),
            value: ManuallyDrop::new(value),
        });

        // ...
    }
}

Starting Operations

Before starting an operation that involves loading atomic pointers, you must mark the thread as active by calling the enter method.

impl Stack {
    pub fn push(&self, value: T) {
        // ...

        let guard = self.collector.enter(); // <===

        // ...
    }
}

Protecting Pointers

enter returns a guard that allows you to safely load atomic pointers. Any valid pointer loaded through a guard is guaranteed to stay valid until the guard is dropped, or is retired by the current thread. Importantly, if another thread retires an object that you protected, the collector knows not to reclaim the object until your guard is dropped.

impl Stack {
    pub fn push(&self, value: T) {
        // ...

        let guard = self.collector.enter();

        loop {
            let head = guard.protect(&self.head, Ordering::Acquire); // <===
            unsafe { (*node).next = head; }

            if self
                .head
                .compare_exchange(head, node, Ordering::Release, Ordering::Relaxed)
                .is_ok()
            {
                break;
            }
        }

        // drop(guard);
    }
}

Note that the lifetime of a guarded pointer is logically tied to that of the guard -- when the guard is dropped the pointer is invalidated -- but a raw pointer is returned for convenience. Datastructures that return shared references to values should ensure that the lifetime of the reference is tied to the lifetime of a guard.

Retiring Objects

Objects that have been removed from a data structure can be safely retired through the collector. It will be reclaimed when no threads holds a reference to it:

impl<T> Stack<T> {
    pub fn pop(&self) -> Option<T> {
        let guard = self.collector.enter(); // <=== mark the thread as active

        loop {
            let head = guard.protect(&self.head, Ordering::Acquire); // <=== safely load the head

            if head.is_null() {
                return None;
            }

            let next = unsafe { (*head).next };

            if self
                .head
                .compare_exchange(head, next, Ordering::Release, Ordering::Relaxed)
                .is_ok()
            {
                unsafe {
                    let data = ptr::read(&(*head).value);
                    self.collector.retire(head, reclaim::boxed::<Node<T>>); // <===
                    return Some(ManuallyDrop::into_inner(data));
                }
            }
        }
    }
}

There are a couple important things to note about retiring an object:

1. Retired objects must be logically removed

An object can only be retired if it is no longer accessible to any thread that comes after. In the above code example this was ensured by swapping out the node before retiring it. Threads that loaded a value before it was retired are safe, but threads that come after are not.

2. Retired objects cannot be accessed by the current thread

Unlike in schemes like EBR, a guard does not protect objects retired by the current thread. If no other thread holds a reference to an object it may be reclaimed immediately. This makes the following code unsound:

let ptr = guard.protect(&node, Ordering::Acquire);
collector.retire(ptr, |_| {});
println!("{}", (*ptr).value); // <===== unsound!

Retirement can be delayed until the guard is dropped by calling retire on the guard, instead of on the collector directly:

let ptr = guard.protect(&node, Ordering::Acquire);
guard.retire(ptr, |_| {});
println!("{}", (*ptr).value); // <===== ok!
drop(guard); // <===== ptr is invalidated

3. Custom Reclaimers

You probably noticed that retire takes a function as a second parameter. This function is known as a reclaimer, and is run when the collector decides it is safe to free the retired object. Typically you will pass in a function from the seize::reclaim module. For example, values allocated with Box can use reclaim::boxed:

use seize::reclaim;

impl<T> Stack<T> {
    pub fn pop(&self) -> Option<T> {
        // ...
        self.collector.retire(head, reclaim::boxed::<Node<T>>); // <===
        // ...
    }
}

The type annotation there is important. It is unsound to pass a reclaimer of a different type than the object being retired.

If you need to run custom reclamation code, you can write a custom reclaimer. Functions passed to retire are called with a type-erased Link. This is because retired values are connected to thread-local batches via linked lists, losing any type information. To extract the underlying value from a link, you can call the cast method.

collector.retire(value, |link: Link| unsafe {
    // SAFETY: the value passed to retire was of type
    // `*mut Linked<Value>`
    let ptr: *mut Linked<Value> = link.cast::<Value>();

    // SAFETY: the value was allocated with `link_boxed`
    let value = Box::from_raw(ptr);

    println!("dropping {}", value);

    drop(value);
});

More Repositories

1

modern-unix

A collection of modern/faster/saner alternatives to common unix commands.
29,534
star
2

matchit

A high performance, zero-copy URL router.
Rust
311
star
3

astra

Rust web servers without async/await.
Rust
161
star
4

boxcar

A concurrent, append-only vector.
Rust
100
star
5

too-many-web-servers

https://ibraheem.ca/posts/too-many-web-servers/
Rust
80
star
6

awaitgroup

Wait for a collection of async tasks to finish.
Rust
28
star
7

dotfiles

My dotfiles for zsh, vim, i3, polybar, alacritty ...
Lua
24
star
8

httprouter-rs

A fast, minimal HTTP framework.
Rust
16
star
9

ripc

A C compiler, written in Rust.
Rust
9
star
10

firefly

High performance concurrent channels.
Rust
6
star
11

derive-alias

Alias mutliple derives as one.
Rust
6
star
12

jiffy

Rust
4
star
13

platoon

A lightweight async runtime for Rust.
Rust
4
star
14

mongo_beautiful_logger

A simple and beautiful logger gem for MongoDB in you Ruby/Rails app.
Ruby
4
star
15

agilely

Agilely is an open source project management solution for individuals and teams alike.
Ruby
4
star
16

bison

A powerful web-application framework that does the heavy lifting for you.
Rust
2
star
17

ibraheemdev

Ibraheem Ahmed | Software developer. Open source enthusiast. Freelancer.
SCSS
1
star
18

turbofish

A fast, executor agnostic HTTP implementation in Rust.
Rust
1
star
19

go-chatrooms-example

This application shows how to use the websocket package to implement a simple web chat application with multiple rooms
Go
1
star
20

seize-hashmap

A Rust concurrent HashMap optimized for read-heavy workloads.
Rust
1
star
21

tictactoe

A react powered tictactoe app
JavaScript
1
star
22

papaya-db

Rust
1
star
23

dangerous-shell-commands

1
star