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}