Skip to main content

pistonite_cu/co/
pool.rs

1use std::sync::{Arc, Weak};
2
3use tokio::sync::Semaphore;
4
5use crate::co::{AbortHandle, Handle, co_util, runtime};
6
7/// Create a new [`Pool`].
8///
9/// - If `capacity > 0`, then the pool will be created
10///   with literally that number of permits (concurrency) available.
11/// - If `capacity = 0`, then the pool will be created
12///   with the number of logical processors on the system
13///   using the `num_cpus` crate, minimum 1.
14/// - if `capacity < 0`, then the pool will be created
15///   with the number of logical processors on the system,
16///   minus the specified amount, and minimum 1.
17///   Use `pool(-1)`, for example, to fire off background async tasks
18///   while the main thread is doing more work.
19#[inline(always)]
20pub fn pool(capacity: isize) -> Pool {
21    match capacity {
22        1.. => Pool::new(capacity as usize),
23        c => {
24            let n = num_cpus::get();
25            let n = n.saturating_sub(-c as usize).max(1);
26            Pool::new(n)
27        }
28    }
29}
30
31/// A pool for limiting async tasks
32///
33/// The pool is not to be used as a thread pool. It is used more as a "limiter"
34/// to ensure we only fire a limited number of resource-intensive tasks.
35/// The IO work for those tasks are drived by a single monitoring thread.
36///
37/// This is good for things like spawning compiler processes where the IO between
38/// child and parent processes are low, but not good for spawning
39/// CPU-bound tasks. Use something like `rayon` for CPU parallelism.
40///
41/// The pool can be cloned and shared between threads. Dropping the pool will not cause the spawned tasks to be either joined
42/// to canceled. You must use individual handles to join them.
43///
44/// This is constructed with [`cu::co::pool`](function@pool),
45/// since it's shorter than `cu::co::Pool::new(n)`.
46#[derive(Clone)]
47pub struct Pool(Arc<PoolInner>);
48struct PoolInner(Semaphore);
49impl Pool {
50    fn new(capacity: usize) -> Self {
51        // this new function is meant to be a plain constructor,
52        // so the extra logic for user-friendly interface
53        // is in the `pool` function.
54        //
55        // this function is also private, so there's only one way
56        // for user to create a Pool.
57        Self(Arc::new(PoolInner(Semaphore::new(capacity))))
58    }
59
60    /// Add N permits to the underlying semaphore
61    pub fn add_permits(&self, n: usize) {
62        self.0.0.add_permits(n);
63    }
64
65    /// Remove N permits from the underlying semaphore,
66    /// return the actual number removed.
67    pub fn forget_permits(&self, n: usize) -> usize {
68        self.0.0.forget_permits(n)
69    }
70
71    /// Spawn a task using the background runtime
72    ///
73    /// The task is spawned with [`cu::co::spawn`](crate::co::spawn),
74    /// and will only start being executed when the pool
75    /// has availability (permits).
76    pub fn spawn<F>(&self, future: F) -> Handle<F::Output>
77    where
78        F: Future + Send + 'static,
79        F::Output: Send + 'static,
80    {
81        let sem = Arc::clone(&self.0);
82        crate::co::spawn(async move {
83            let _permit = sem.0.acquire().await.ok();
84            let result = future.await;
85            drop(_permit);
86            result
87        })
88    }
89
90    /// Spawn a synchronous task using the background runtime
91    ///
92    /// A wait task will be spawned with [`cu::co::spawn`](crate::co::spawn),
93    /// and the function will only be spawned with `cu::co::spawn_blocking`
94    /// when the pool has availability (permits).
95    ///
96    /// # Performance
97    /// Even though it's never recommended to block the async runtime,
98    /// if the synchronous work is short, it might be more performant
99    /// to use `spawn` instead.
100    #[cfg(feature = "coroutine-heavy")]
101    pub fn spawn_blocking<F, R>(&self, f: F) -> Handle<crate::Result<R>>
102    where
103        F: FnOnce() -> R + Send + 'static,
104        R: Send + 'static,
105    {
106        let sem = Arc::clone(&self.0);
107        crate::co::spawn(async move {
108            let _permit = sem.0.acquire().await.ok();
109            let result = crate::co::spawn_blocking(f).co_join().await;
110            drop(_permit);
111            result
112        })
113    }
114}
115
116/// Create a set of handles, that can be joined concurrently.
117///
118/// Typically, you would spawn the tasks and collect the handles in a `Vec`,
119/// and convert it to a set with this function. Later, you can use [`Set::extend`]
120/// to add more handles.
121pub fn set<T: Send + 'static, I: IntoIterator<Item = Handle<T>>>(iter: I) -> Set<T> {
122    let mut set = Set::new();
123    set.extend(iter);
124    set
125}
126
127/// Create a set of handles, that can be joined concurrently.
128///
129/// The input handles are allowed to contain errors. Those errors
130/// will be collected and returned along with the set in an `Err`.
131/// If there are no errors, `Ok` is returned.
132///
133/// Typically, you would spawn the tasks and collect the handles in a `Vec`,
134/// and convert it to a set with this function. Later, you can use [`Set::extend`]
135/// to add more handles.
136pub fn set_flatten<T: Send + 'static, E, I: IntoIterator<Item = Result<Handle<T>, E>>>(
137    iter: I,
138) -> Result<Set<T>, (Set<T>, Vec<E>)> {
139    let mut set = Set::new();
140    let errors = set.extend_flatten(iter);
141    if errors.is_empty() {
142        Ok(set)
143    } else {
144        Err((set, errors))
145    }
146}
147
148/// A set of [`Handle`]s that can be joined concurrently.
149///
150/// When the set is dropped, all handles added to the set
151/// are aborted.
152pub struct Set<T> {
153    join_set: tokio::task::JoinSet<crate::Result<T>>,
154    abort_handles: Vec<Weak<AbortHandle>>,
155}
156impl<T> Drop for Set<T> {
157    fn drop(&mut self) {
158        for x in &self.abort_handles {
159            if let Some(x) = x.upgrade() {
160                x.abort()
161            }
162        }
163    }
164}
165impl<T: Send + 'static> Set<T> {
166    fn new() -> Self {
167        Self {
168            join_set: tokio::task::JoinSet::new(),
169            abort_handles: Vec::new(),
170        }
171    }
172
173    /// Add one handle to the set.
174    ///
175    /// A task will be spawned onto the background runtime to start joining
176    /// the handle. Therefore, when there are a lot of handles,
177    /// it's better to collect them first into a Vec, then use [`extend`](Self::extend)
178    /// to add them to the set at once, to put slightly less pressure on the runtime.
179    ///
180    /// The handle will also become not abortable, but dropping the Set will
181    /// abort all handles.
182    pub fn add(&mut self, handle: Handle<T>) {
183        self.gc_handles(Some(1));
184        self.add_internal(handle);
185    }
186
187    /// Add multiple handles to the set
188    ///
189    /// The handles will become not abortable, but dropping the Set will
190    /// abort all handles.
191    pub fn extend<I: IntoIterator<Item = Handle<T>>>(&mut self, iter: I) {
192        let iter = iter.into_iter();
193        self.gc_handles(iter.size_hint().1);
194        for handle in iter {
195            self.add_internal(handle);
196        }
197    }
198
199    /// Add multiple handles to the set, from a set of results, the errors are collected
200    /// and returned.
201    ///
202    /// The handles will become not abortable, but dropping the Set will
203    /// abort all handles.
204    pub fn extend_flatten<E, I: IntoIterator<Item = Result<Handle<T>, E>>>(
205        &mut self,
206        iter: I,
207    ) -> Vec<E> {
208        let iter = iter.into_iter();
209        let mut errors = vec![];
210        self.gc_handles(iter.size_hint().1);
211        for handle in iter {
212            match handle {
213                Ok(handle) => self.add_internal(handle),
214                Err(e) => errors.push(e),
215            }
216        }
217        errors
218    }
219
220    fn add_internal(&mut self, handle: Handle<T>) {
221        let abort_handle = Arc::new(handle.abort_handle());
222        self.abort_handles.push(Arc::downgrade(&abort_handle));
223        self.join_set.spawn_on(
224            async move {
225                let result = handle.co_join().await;
226                // allow the abort handle to be cleaned up later
227                drop(abort_handle);
228                result
229            },
230            runtime::background().handle(),
231        );
232    }
233
234    fn gc_handles(&mut self, add: Option<usize>) {
235        // gc whenever we are about to grow
236        if let Some(add) = add
237            && self.abort_handles.capacity() - self.abort_handles.len() >= add
238        {
239            return;
240        }
241        self.abort_handles.retain(|x| x.strong_count() != 0);
242    }
243
244    /// Wait for the next handle to be available, and return it's result,
245    /// or `None` if the set is empty, meaning all handles are joined.
246    pub async fn next(&mut self) -> Option<crate::Result<T>> {
247        let result = self.join_set.join_next().await?;
248        match result {
249            Err(join_error) => match co_util::handle_join_error(join_error) {
250                Err(e) => Some(Err(e)),
251                Ok(_) => Some(Err(crate::fmterr!("aborted"))),
252            },
253            Ok(x) => Some(x),
254        }
255    }
256
257    /// Wait for the next handle to be available, and return it's result,
258    /// or `None` if the set is empty, meaning all handles are joined.
259    ///
260    /// # Blocking
261    /// **Do not use this in an async context**, since it will block the runtime,
262    /// and will panic if the thread is currently driving IO.
263    /// Use [`next().await`](`Self::next`) instead.
264    #[inline]
265    pub fn block(&mut self) -> Option<crate::Result<T>> {
266        runtime::foreground().block_on(async move { self.next().await })
267    }
268}