Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
threadexio committed Jan 6, 2024
0 parents commit dc98c47
Show file tree
Hide file tree
Showing 15 changed files with 707 additions and 0 deletions.
16 changes: 16 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# EditorConfig is awesome: https://EditorConfig.org

# top-most EditorConfig file
root = true

[*]
indent_style = tab
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.yaml]
indent_style = space
indent_size = 2
30 changes: 30 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: CI

on:
push:
pull_request:

jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ ubuntu, windows ]
runs-on: ${{ matrix.os }}-latest
name: Test on ${{ matrix.os }}-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo test

doc:
name: Docs
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- uses: dtolnay/install@cargo-docs-rs
- run: cargo docs-rs

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/.vscode
/Cargo.lock
/target
17 changes: 17 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "rcurs"
version = "0.1.0"
edition = "2021"
description = "An oxidized RCU implementation"
license = "MIT"
repository = "https://github.com/threadexio/rcurs"
readme = "README.md"
keywords = ["rcu", "synchronization", "concurrent", "parallel", "lock-free"]
categories = ["concurrency"]

[dependencies]

[features]
default = ["std"]

std = []
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 1337

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.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[`core::hint::spin_loop()`]: https://doc.rust-lang.org/stable/core/hint/fn.spin_loop.html
[`Condvar`]: https://doc.rust-lang.org/stable/std/sync/struct.Condvar.html

# rcurs

A simple [RCU](https://en.wikipedia.org/wiki/Read-copy-update) with an oxidized interface. Read more at the [docs](https://docs.rs/rcurs).

The crate supports running both with or without the `std` library but has a hard dependency on `alloc`. If your environment allows, you should try to keep the `std` feature enabled as that contains typically more efficient implementations of blocking primitives.

Without the `std` feature, the only way to block is to spin in place using whatever optimization [`core::hint::spin_loop()`] can provide. But with the standard library, blocking is done using [`Condvar`]s. [`Condvar`]s call out to the kernel for blocking. The kernel can then choose what is best, spin itself, or usually give control back to the scheduler to run other processes.

## Features

- `std`: Enable use of primitives in the standard library
4 changes: 4 additions & 0 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[toolchain]
channel = "stable"
profile = "minimal"
components = ["clippy", "rustfmt"]
23 changes: 23 additions & 0 deletions rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
max_width = 70
hard_tabs = true
tab_spaces = 4
newline_style = "Unix"
use_small_heuristics = "Off"
fn_call_width = 60
attr_fn_like_width = 60
struct_lit_width = 60
struct_variant_width = 60
array_width = 60
chain_width = 60
single_line_if_else_max_width = 0
reorder_imports = true
reorder_modules = true
remove_nested_parens = true
match_arm_leading_pipes = "Never"
fn_params_layout = "Tall"
match_block_trailing_comma = true
edition = "2021"
merge_derives = true
use_try_shorthand = true
use_field_init_shorthand = true
force_explicit_abi = true
10 changes: 10 additions & 0 deletions src/cfg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
macro_rules! cfg_std {
($($item:item)*) => {
$(
#[cfg(feature = "std")]
$item
)*
};
}

pub(crate) use cfg_std;
113 changes: 113 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//! [`Rcu`] or Read-Copy-Update is a mechanism that allows sharing and updating
//! a piece of data while multiple parallel code blocks have access to it in a
//! lock-free manner. A better explanation is available at the [kernel docs](https://www.kernel.org/doc/html/latest/RCU/whatisRCU.html)
//! or even [Wikipedia](https://en.wikipedia.org/wiki/Read-copy-update).
//!
//! Internally, an RCU is composed of an _atomic_ pointer (the atomic part
//! is important) to some data along with the number of currently active
//! references to it. Something like a garbage-collector (but obviously way
//! simpler). So when you want to get at the actual data, you simply increment
//! the counter of references by one and decrement it when you are done. This
//! is the easy part, now how do we update the value without locking and
//! without messing up anyone already working? Well, each reference does not
//! keep a pointer to the RCU, but instead copies the value of the atomic
//! pointer when it is created. The key is that the pointer to the data is
//! atomic. So we simply create a new copy of data, make our changes to that
//! copy, and then atomically update the pointer. This way we ensure that a
//! reference that is created at the same time as we update it uses either:
//! the old value or the new value. In any case, it does not end up with an
//! invalid pointer. Because we are a model programmer, we must also not
//! forget to clean up the old data which is effectively outdated and useless.
//! But we can't just clean up the old data: there might be references
//! to it from before we did the update. Ah, so we will wait for the code
//! with old references to drop them, and because we swapped the pointer before,
//! no new references can get access to the old data. After waiting and ensuring
//! there are no remaining references to the old data, we can now safely free
//! it without worry.
//!
//! # Example
//!
//! ```rust,no_run
//! use std::thread;
//! use std::time::Duration;
//!
//! type Rcu<T> = rcurs::Rcu<T, rcurs::Spin>;
//!
//! #[derive(Debug, Clone, PartialEq, Eq)]
//! struct User {
//! uid: i32,
//! gid: i32,
//! }
//!
//! fn setugid(user: &Rcu<User>, uid: i32, gid: i32) {
//! let mut new = user.get().clone();
//!
//! if new.uid == uid && new.gid == gid {
//! return;
//! }
//!
//! new.uid = uid;
//! new.gid = gid;
//!
//! user.update(new);
//! }
//!
//! // Basically a `sleep`` function that holds onto `user` and prints it after
//! // `sec` seconds have passed.
//! fn compute(user: &Rcu<User>, id: &str, sec: u64) {
//! let user = user.get();
//! thread::sleep(Duration::from_secs(sec));
//! println!("compute[{id}]: finish work for {}:{}", user.uid, user.gid);
//! }
//!
//! fn thread_1(user: &Rcu<User>) {
//! compute(user, "1", 3);
//!
//! // This call will update `user`.
//! setugid(user, 1000, 1000);
//!
//! compute(user, "3", 4);
//! }
//!
//! fn thread_2(user: &Rcu<User>) {
//! // The following compute call will always see the `User { uid: 0, gid: 0}`
//! // as the `setugid` call happens 3 seconds after it has started executing.
//! compute(user, "2", 5);
//! }
//!
//! fn main() {
//! let user = Rcu::new(User { uid: 0, gid: 0 });
//!
//! thread::scope(|scope| {
//! scope.spawn(|| thread_1(&user));
//! scope.spawn(|| thread_2(&user));
//! });
//! }
//! ```
#![deny(missing_docs)]
#![warn(
clippy::all,
clippy::correctness,
clippy::pedantic,
clippy::cargo,
clippy::nursery,
clippy::perf,
clippy::style
)]
#![allow(
clippy::missing_panics_doc,
clippy::significant_drop_tightening,
clippy::needless_lifetimes
)]
#![cfg_attr(not(feature = "std"), no_std)]

mod cfg;

mod notify;
mod rcu;

#[doc(inline)]
pub use self::notify::*;

#[doc(inline)]
pub use self::rcu::{Guard, Rcu};
49 changes: 49 additions & 0 deletions src/notify/blocking.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use super::Notify;

use std::sync::{Condvar, Mutex};

/// A [`Notify`] backend that uses a [`Condvar`] to achieve true blocking.
pub struct Blocking {
/* Keep track of both whether notify was called and how many waiter there are.
* This way, the waiter who wakes up last can know to reset the `notified` flag
* again to prepare for the next `notify`.
*/
lock: Mutex<(bool, u8)>,
var: Condvar,
}

impl Notify for Blocking {
fn new() -> Self {
Self {
lock: Mutex::new((false, 0)),
var: Condvar::new(),
}
}

fn wait(&self) {
let mut guard = self.lock.lock().unwrap();
guard.1 += 1;

let mut guard = self
.var
.wait_while(guard, |(notified, _)| !*notified)
.unwrap();
guard.1 -= 1;

if guard.1 == 0 {
guard.0 = false;
}
}

fn notify(&self) {
let mut guard = self.lock.lock().unwrap();
guard.0 = true;
self.var.notify_all();
}
}

impl Default for Blocking {
fn default() -> Self {
Self::new()
}
}
Loading

0 comments on commit dc98c47

Please sign in to comment.