Skip to content

Commit c1344a7

Browse files
Add async functionality
This new feature is gated behind the `async` feature flag. It allows to asynchronously await for a pool slot to become available, making it easier to share constrained resources between multiple tasks. It requires the `AtomicWaker` functionality from the `embassy-sync` crate, which in turn requires a critical section implementation.
1 parent d93c469 commit c1344a7

File tree

10 files changed

+362
-27
lines changed

10 files changed

+362
-27
lines changed

.github/workflows/rust-miri.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@ jobs:
2020
override: true
2121
components: miri
2222
- name: Test
23-
run: cargo miri test
23+
run: cargo miri test --all-features

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## 1.1.0 - 2024-08-19
9+
10+
- Add async functionality
11+
812
## 1.0.1 - 2022-12-11
913

1014
- Use `atomic-polyfill`, to support targets without atomic CAS.

Cargo.toml

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,33 @@
11
[package]
2-
name = "atomic-pool"
3-
version = "1.0.1"
4-
authors = ["Dario Nieuwenhuis <[email protected]>"]
5-
description = "Statically allocated pool providing a std-like Box."
6-
repository = "https://github.com/embassy-rs/atomic-pool"
2+
name = "async-pool"
3+
version = "1.1.0"
4+
authors = ["Daniel Stuart <[email protected]>"]
5+
description = "Statically allocated pool providing a std-like Box, with async functionality."
6+
repository = "https://github.com/danielstuart14/async-pool"
77
edition = "2021"
88
readme = "README.md"
99
license = "MIT OR Apache-2.0"
10-
categories = [
11-
"embedded",
12-
"no-std",
13-
"concurrency",
14-
"memory-management",
15-
]
10+
categories = ["embedded", "no-std", "concurrency", "memory-management"]
1611

1712
[dependencies]
1813
atomic-polyfill = "1.0"
1914
as-slice-01 = { package = "as-slice", version = "0.1.5" }
2015
as-slice-02 = { package = "as-slice", version = "0.2.1" }
2116
stable_deref_trait = { version = "1.2.0", default-features = false }
17+
embassy-sync = "0.6.0"
18+
19+
# Used by async tests and examples
20+
[dev-dependencies]
21+
embassy-executor = { version = "0.6.0", features = [
22+
"arch-std",
23+
"executor-thread",
24+
"integrated-timers",
25+
"task-arena-size-32768",
26+
] }
27+
embassy-time = { version = "0.3.2", features = ["std"] }
28+
embassy-futures = "0.1.1"
29+
critical-section = { version = "1.1", features = ["std"] }
30+
tokio = { version = "1", features = ["full"] }
31+
32+
[features]
33+
default = []

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
1-
# atomic-pool
1+
# async-pool
22

3-
[![Documentation](https://docs.rs/atomic-pool/badge.svg)](https://docs.rs/atomic-pool)
3+
[![Documentation](https://docs.rs/async-pool/badge.svg)](https://docs.rs/async-pool)
44

5-
Statically allocated pool providing a std-like Box.
5+
Statically allocated pool providing a std-like Box, with hability to asynchronously wait for a pool slot to become available.
6+
7+
This crate is tailored to be used with no-std async runtimes, like [Embassy](https://embassy.dev/), but can also be used in std environments (check examples).
8+
9+
## Dependencies
10+
11+
This crate uses the `AtomicWaker` functionality from `embassy-sync` crate, which in turn requires a critical section implementation. Check [critical-section](https://crates.io/crates/critical-section).
12+
13+
## Previous work
14+
15+
This crate is heavily based on [atomic-pool](https://github.com/embassy-rs/atomic-pool).
616

717
## License
818

examples/embassy.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//! This example demonstrates the use of the `async_pool` crate with the `embassy` executor.
2+
//! The example is meant to be run on std (println, process::exit), but can easily be adapted to run on a no_std environment.
3+
4+
use embassy_executor::Spawner;
5+
use embassy_futures::join::join5;
6+
use embassy_time::Timer;
7+
8+
use core::mem;
9+
10+
use async_pool::{pool, Box};
11+
12+
#[derive(Debug)]
13+
#[allow(dead_code)]
14+
struct Packet(u32);
15+
16+
// A maximum of 2 Packet instances can be allocated at a time.
17+
// A maximum of 1 future can be waiting at a time.
18+
pool!(PacketPool: [Packet; 2], 1);
19+
20+
#[embassy_executor::task]
21+
async fn run() {
22+
// Allocate non-blocking
23+
let fut1 = async {
24+
println!("1 - allocating async...");
25+
let box1 = Box::<PacketPool>::new(Packet(1));
26+
println!("1 - allocated: {:?}", box1);
27+
Timer::after_millis(100).await;
28+
println!("1 - dropping allocation...");
29+
mem::drop(box1);
30+
};
31+
32+
// Allocate asynchronously
33+
let fut2 = async {
34+
Timer::after_millis(5).await;
35+
println!("2 - allocating sync...");
36+
let box2 = Box::<PacketPool>::new_async(Packet(2)).await;
37+
println!("2 - allocated: {:?}", box2);
38+
Timer::after_millis(150).await;
39+
println!("2 - dropping allocation...");
40+
mem::drop(box2);
41+
};
42+
43+
// Allocate non-blocking (fails, data pool is full)
44+
let fut3 = async {
45+
Timer::after_millis(10).await;
46+
println!("3 - allocating sync...");
47+
let box3 = Box::<PacketPool>::new(Packet(3));
48+
println!(
49+
"3 - allocation fails because the data pool is full: {:?}",
50+
box3
51+
);
52+
};
53+
54+
// Allocate asynchronously (waits for a deallocation)
55+
let fut4 = async {
56+
Timer::after_millis(15).await;
57+
println!("4 - allocating async...");
58+
let box4 = Box::<PacketPool>::new_async(Packet(4)).await;
59+
println!("4 - allocated: {:?}", box4);
60+
Timer::after_millis(100).await;
61+
println!("4 - dropping allocation...");
62+
};
63+
64+
// Allocate asynchronously (fails, waker pool is full)
65+
let fut5 = async {
66+
Timer::after_millis(20).await;
67+
println!("5 - allocating async...");
68+
let box5 = Box::<PacketPool>::new_async(Packet(5)).await;
69+
println!(
70+
"5 - allocation fails because the waker pool is full: {:?}",
71+
box5
72+
);
73+
};
74+
75+
join5(fut1, fut2, fut3, fut4, fut5).await;
76+
std::process::exit(0); // Exit the executor
77+
}
78+
79+
#[embassy_executor::main]
80+
async fn main(spawner: Spawner) {
81+
spawner.spawn(run()).unwrap();
82+
}

examples/simple.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
use std::mem;
1+
//! This example demonstrates the use of the `async_pool` crate without any async executor.
2+
use core::mem;
23

3-
use atomic_pool::{pool, Box};
4+
use async_pool::{pool, Box};
45

56
#[derive(Debug)]
7+
#[allow(dead_code)]
68
struct Packet(u32);
79

8-
pool!(PacketPool: [Packet; 4]);
10+
pool!(PacketPool: [Packet; 4], 0);
911

1012
fn main() {
1113
let box1 = Box::<PacketPool>::new(Packet(1));

examples/tokio.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
//! This example demonstrates the use of the `async_pool` crate with the `tokio` framework.
2+
3+
use tokio::time::sleep;
4+
use tokio::join;
5+
6+
use std::mem;
7+
use std::time::Duration;
8+
9+
use async_pool::{pool, Box};
10+
11+
#[derive(Debug)]
12+
#[allow(dead_code)]
13+
struct Packet(u32);
14+
15+
// A maximum of 2 Packet instances can be allocated at a time.
16+
// A maximum of 1 future can be waiting at a time.
17+
pool!(PacketPool: [Packet; 2], 1);
18+
19+
#[tokio::main]
20+
async fn main() {
21+
// Allocate non-blocking
22+
let fut1 = async {
23+
println!("1 - allocating async...");
24+
let box1 = Box::<PacketPool>::new(Packet(1));
25+
println!("1 - allocated: {:?}", box1);
26+
sleep(Duration::from_millis(100)).await;
27+
println!("1 - dropping allocation...");
28+
mem::drop(box1);
29+
};
30+
31+
// Allocate asynchronously
32+
let fut2 = async {
33+
sleep(Duration::from_millis(5)).await;
34+
println!("2 - allocating sync...");
35+
let box2 = Box::<PacketPool>::new_async(Packet(2)).await;
36+
println!("2 - allocated: {:?}", box2);
37+
sleep(Duration::from_millis(150)).await;
38+
println!("2 - dropping allocation...");
39+
mem::drop(box2);
40+
};
41+
42+
// Allocate non-blocking (fails, data pool is full)
43+
let fut3 = async {
44+
sleep(Duration::from_millis(10)).await;
45+
println!("3 - allocating sync...");
46+
let box3 = Box::<PacketPool>::new(Packet(3));
47+
println!(
48+
"3 - allocation fails because the data pool is full: {:?}",
49+
box3
50+
);
51+
};
52+
53+
// Allocate asynchronously (waits for a deallocation)
54+
let fut4 = async {
55+
sleep(Duration::from_millis(15)).await;
56+
println!("4 - allocating async...");
57+
let box4 = Box::<PacketPool>::new_async(Packet(4)).await;
58+
println!("4 - allocated: {:?}", box4);
59+
sleep(Duration::from_millis(100)).await;
60+
println!("4 - dropping allocation...");
61+
};
62+
63+
// Allocate asynchronously (fails, waker pool is full)
64+
let fut5 = async {
65+
sleep(Duration::from_millis(20)).await;
66+
println!("5 - allocating async...");
67+
let box5 = Box::<PacketPool>::new_async(Packet(5)).await;
68+
println!(
69+
"5 - allocation fails because the waker pool is full: {:?}",
70+
box5
71+
);
72+
};
73+
74+
join!(fut1, fut2, fut3, fut4, fut5);
75+
}

src/atomic_bitset.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
pub mod droppable_bit;
2+
13
use atomic_polyfill::{AtomicU32, Ordering};
24

5+
/// A bitset that can be used to allocate slots in a pool
36
pub struct AtomicBitset<const N: usize, const K: usize>
47
where
58
[AtomicU32; K]: Sized,
@@ -16,6 +19,10 @@ where
1619
Self { used: [Z; K] }
1720
}
1821

22+
pub fn alloc_droppable(&self) -> Option<droppable_bit::DroppableBit<N, K>> {
23+
self.alloc().map(|i| droppable_bit::DroppableBit::new(self, i))
24+
}
25+
1926
pub fn alloc(&self) -> Option<usize> {
2027
for (i, val) in self.used.iter().enumerate() {
2128
let mut allocated = 0;

src/atomic_bitset/droppable_bit.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
use super::AtomicBitset;
2+
3+
/// Automatically frees the Bitset slot when DroppableBit is dropped
4+
/// Useful for async environments where the future might be dropped before it completes
5+
pub struct DroppableBit<'a, const N: usize, const K: usize> {
6+
bitset: &'a AtomicBitset<N, K>,
7+
inner: usize,
8+
}
9+
10+
impl<'a, const N: usize, const K: usize> DroppableBit<'a, N, K> {
11+
/// Only a single instance of DroppableBit should be created for each slot
12+
/// Restrict it to only be created by AtomicBitset `alloc_droppable` method
13+
pub(super) fn new(bitset: &'a AtomicBitset<N, K>, inner: usize) -> Self {
14+
Self { bitset, inner }
15+
}
16+
17+
pub fn inner(&self) -> usize {
18+
self.inner
19+
}
20+
}
21+
22+
impl<const N: usize, const K: usize> Drop for DroppableBit<'_, N, K> {
23+
fn drop(&mut self) {
24+
self.bitset.free(self.inner);
25+
}
26+
}
27+
28+
#[cfg(test)]
29+
mod test {
30+
use super::*;
31+
32+
#[test]
33+
fn test_16() {
34+
let s = AtomicBitset::<16, 1>::new();
35+
let mut v = vec![];
36+
37+
for _ in 0..16 {
38+
let bit = s.alloc().map(|i| DroppableBit::new(&s, i));
39+
assert!(bit.is_some());
40+
41+
v.push(bit.unwrap());
42+
}
43+
assert_eq!(s.alloc(), None);
44+
v.pop();
45+
v.pop();
46+
assert!(s.alloc().is_some());
47+
assert!(s.alloc().is_some());
48+
assert_eq!(s.alloc(), None);
49+
v.pop();
50+
assert!(s.alloc().is_some());
51+
}
52+
}

0 commit comments

Comments
 (0)