tokio_util/task/spawn_pinned.rs
1use futures_util::future::{AbortHandle, Abortable};
2use std::fmt;
3use std::fmt::{Debug, Formatter};
4use std::future::Future;
5use std::sync::atomic::{AtomicUsize, Ordering};
6use std::sync::Arc;
7use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
8use tokio::sync::oneshot;
9use tokio::task::{spawn_local, JoinHandle};
10
11/// A cloneable handle to a local pool, used for spawning `!Send` tasks.
12///
13/// Internally the local pool uses a [`tokio::runtime::LocalRuntime`] for each worker thread
14/// in the pool. Consequently you can also use [`tokio::task::spawn_local`] (which will
15/// execute on the same thread) inside the Future you supply to the various spawn methods
16/// of `LocalPoolHandle`.
17///
18/// [`tokio::runtime::LocalRuntime`]: tokio::runtime::LocalRuntime
19/// [`tokio::task::spawn_local`]: tokio::task::spawn_local
20///
21/// # Examples
22///
23/// ```
24/// # #[cfg(not(target_family = "wasm"))]
25/// # {
26/// use std::rc::Rc;
27/// use tokio::task;
28/// use tokio_util::task::LocalPoolHandle;
29///
30/// #[tokio::main(flavor = "current_thread")]
31/// async fn main() {
32/// let pool = LocalPoolHandle::new(5);
33///
34/// let output = pool.spawn_pinned(|| {
35/// // `data` is !Send + !Sync
36/// let data = Rc::new("local data");
37/// let data_clone = data.clone();
38///
39/// async move {
40/// task::spawn_local(async move {
41/// println!("{}", data_clone);
42/// });
43///
44/// data.to_string()
45/// }
46/// }).await.unwrap();
47/// println!("output: {}", output);
48/// }
49/// # }
50/// ```
51///
52#[derive(Clone)]
53pub struct LocalPoolHandle {
54 pool: Arc<LocalPool>,
55}
56
57impl LocalPoolHandle {
58 /// Create a new pool of threads to handle `!Send` tasks. Spawn tasks onto this
59 /// pool via [`LocalPoolHandle::spawn_pinned`].
60 ///
61 /// # Panics
62 ///
63 /// Panics if the pool size is less than one.
64 #[track_caller]
65 pub fn new(pool_size: usize) -> LocalPoolHandle {
66 assert!(pool_size > 0);
67
68 let workers = (0..pool_size)
69 .map(|_| LocalWorkerHandle::new_worker())
70 .collect();
71
72 let pool = Arc::new(LocalPool { workers });
73
74 LocalPoolHandle { pool }
75 }
76
77 /// Returns the number of threads of the Pool.
78 #[inline]
79 pub fn num_threads(&self) -> usize {
80 self.pool.workers.len()
81 }
82
83 /// Returns the number of tasks scheduled on each worker. The indices of the
84 /// worker threads correspond to the indices of the returned `Vec`.
85 pub fn get_task_loads_for_each_worker(&self) -> Vec<usize> {
86 self.pool
87 .workers
88 .iter()
89 .map(|worker| worker.task_count.load(Ordering::SeqCst))
90 .collect::<Vec<_>>()
91 }
92
93 /// Spawn a task onto a worker thread and pin it there so it can't be moved
94 /// off of the thread. Note that the future is not [`Send`], but the
95 /// [`FnOnce`] which creates it is.
96 ///
97 /// # Examples
98 /// ```
99 /// # #[cfg(not(target_family = "wasm"))]
100 /// # {
101 /// use std::rc::Rc;
102 /// use tokio_util::task::LocalPoolHandle;
103 ///
104 /// #[tokio::main]
105 /// async fn main() {
106 /// // Create the local pool
107 /// let pool = LocalPoolHandle::new(1);
108 ///
109 /// // Spawn a !Send future onto the pool and await it
110 /// let output = pool
111 /// .spawn_pinned(|| {
112 /// // Rc is !Send + !Sync
113 /// let local_data = Rc::new("test");
114 ///
115 /// // This future holds an Rc, so it is !Send
116 /// async move { local_data.to_string() }
117 /// })
118 /// .await
119 /// .unwrap();
120 ///
121 /// assert_eq!(output, "test");
122 /// }
123 /// # }
124 /// ```
125 pub fn spawn_pinned<F, Fut>(&self, create_task: F) -> JoinHandle<Fut::Output>
126 where
127 F: FnOnce() -> Fut,
128 F: Send + 'static,
129 Fut: Future + 'static,
130 Fut::Output: Send + 'static,
131 {
132 self.pool
133 .spawn_pinned(create_task, WorkerChoice::LeastBurdened)
134 }
135
136 /// Differs from `spawn_pinned` only in that you can choose a specific worker thread
137 /// of the pool, whereas `spawn_pinned` chooses the worker with the smallest
138 /// number of tasks scheduled.
139 ///
140 /// A worker thread is chosen by index. Indices are 0 based and the largest index
141 /// is given by `num_threads() - 1`
142 ///
143 /// # Panics
144 ///
145 /// This method panics if the index is out of bounds.
146 ///
147 /// # Examples
148 ///
149 /// This method can be used to spawn a task on all worker threads of the pool:
150 ///
151 /// ```
152 /// # #[cfg(not(target_family = "wasm"))]
153 /// # {
154 /// use tokio_util::task::LocalPoolHandle;
155 ///
156 /// #[tokio::main]
157 /// async fn main() {
158 /// const NUM_WORKERS: usize = 3;
159 /// let pool = LocalPoolHandle::new(NUM_WORKERS);
160 /// let handles = (0..pool.num_threads())
161 /// .map(|worker_idx| {
162 /// pool.spawn_pinned_by_idx(
163 /// || {
164 /// async {
165 /// "test"
166 /// }
167 /// },
168 /// worker_idx,
169 /// )
170 /// })
171 /// .collect::<Vec<_>>();
172 ///
173 /// for handle in handles {
174 /// handle.await.unwrap();
175 /// }
176 /// }
177 /// # }
178 /// ```
179 ///
180 #[track_caller]
181 pub fn spawn_pinned_by_idx<F, Fut>(&self, create_task: F, idx: usize) -> JoinHandle<Fut::Output>
182 where
183 F: FnOnce() -> Fut,
184 F: Send + 'static,
185 Fut: Future + 'static,
186 Fut::Output: Send + 'static,
187 {
188 self.pool
189 .spawn_pinned(create_task, WorkerChoice::ByIdx(idx))
190 }
191}
192
193impl Debug for LocalPoolHandle {
194 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
195 f.write_str("LocalPoolHandle")
196 }
197}
198
199enum WorkerChoice {
200 LeastBurdened,
201 ByIdx(usize),
202}
203
204struct LocalPool {
205 workers: Box<[LocalWorkerHandle]>,
206}
207
208impl LocalPool {
209 /// Spawn a `?Send` future onto a worker
210 #[track_caller]
211 fn spawn_pinned<F, Fut>(
212 &self,
213 create_task: F,
214 worker_choice: WorkerChoice,
215 ) -> JoinHandle<Fut::Output>
216 where
217 F: FnOnce() -> Fut,
218 F: Send + 'static,
219 Fut: Future + 'static,
220 Fut::Output: Send + 'static,
221 {
222 let (sender, receiver) = oneshot::channel();
223 let (worker, job_guard) = match worker_choice {
224 WorkerChoice::LeastBurdened => self.find_and_incr_least_burdened_worker(),
225 WorkerChoice::ByIdx(idx) => self.find_worker_by_idx(idx),
226 };
227 let worker_spawner = worker.spawner.clone();
228
229 // Spawn a future onto the worker's runtime so we can immediately return
230 // a join handle.
231 worker.runtime_handle.spawn(async move {
232 // Move the job guard into the task
233 let _job_guard = job_guard;
234
235 // Propagate aborts via Abortable/AbortHandle
236 let (abort_handle, abort_registration) = AbortHandle::new_pair();
237 let _abort_guard = AbortGuard(abort_handle);
238
239 // Inside the future we can't run spawn_local yet because we're not
240 // in the context of a LocalRuntime. We need to send create_task to the
241 // LocalRuntime task for spawning.
242 let spawn_task = Box::new(move || {
243 // Once we're in the LocalRuntime context we can call spawn_local
244 let join_handle =
245 spawn_local(
246 async move { Abortable::new(create_task(), abort_registration).await },
247 );
248
249 // Send the join handle back to the spawner. If sending fails,
250 // we assume the parent task was canceled, so cancel this task
251 // as well.
252 if let Err(join_handle) = sender.send(join_handle) {
253 join_handle.abort()
254 }
255 });
256
257 // Send the callback to the LocalRuntime task
258 if let Err(e) = worker_spawner.send(spawn_task) {
259 // Propagate the error as a panic in the join handle.
260 panic!("Failed to send job to worker: {e}");
261 }
262
263 // Wait for the task's join handle
264 let join_handle = match receiver.await {
265 Ok(handle) => handle,
266 Err(e) => {
267 // We sent the task successfully, but failed to get its
268 // join handle... We assume something happened to the worker
269 // and the task was not spawned. Propagate the error as a
270 // panic in the join handle.
271 panic!("Worker failed to send join handle: {e}");
272 }
273 };
274
275 // Wait for the task to complete
276 let join_result = join_handle.await;
277
278 match join_result {
279 Ok(Ok(output)) => output,
280 Ok(Err(_)) => {
281 // Pinned task was aborted. But that only happens if this
282 // task is aborted. So this is an impossible branch.
283 unreachable!(
284 "Reaching this branch means this task was previously \
285 aborted but it continued running anyways"
286 )
287 }
288 Err(e) => {
289 if e.is_panic() {
290 std::panic::resume_unwind(e.into_panic());
291 } else if e.is_cancelled() {
292 // No one else should have the join handle, so this is
293 // unexpected. Forward this error as a panic in the join
294 // handle.
295 panic!("spawn_pinned task was canceled: {e}");
296 } else {
297 // Something unknown happened (not a panic or
298 // cancellation). Forward this error as a panic in the
299 // join handle.
300 panic!("spawn_pinned task failed: {e}");
301 }
302 }
303 }
304 })
305 }
306
307 /// Find the worker with the least number of tasks, increment its task
308 /// count, and return its handle. Make sure to actually spawn a task on
309 /// the worker so the task count is kept consistent with load.
310 ///
311 /// A job count guard is also returned to ensure the task count gets
312 /// decremented when the job is done.
313 fn find_and_incr_least_burdened_worker(&self) -> (&LocalWorkerHandle, JobCountGuard) {
314 loop {
315 let (worker, task_count) = self
316 .workers
317 .iter()
318 .map(|worker| (worker, worker.task_count.load(Ordering::SeqCst)))
319 .min_by_key(|&(_, count)| count)
320 .expect("There must be more than one worker");
321
322 // Make sure the task count hasn't changed since when we choose this
323 // worker. Otherwise, restart the search.
324 if worker
325 .task_count
326 .compare_exchange(
327 task_count,
328 task_count + 1,
329 Ordering::SeqCst,
330 Ordering::Relaxed,
331 )
332 .is_ok()
333 {
334 return (worker, JobCountGuard(Arc::clone(&worker.task_count)));
335 }
336 }
337 }
338
339 #[track_caller]
340 fn find_worker_by_idx(&self, idx: usize) -> (&LocalWorkerHandle, JobCountGuard) {
341 let worker = &self.workers[idx];
342 worker.task_count.fetch_add(1, Ordering::SeqCst);
343
344 (worker, JobCountGuard(Arc::clone(&worker.task_count)))
345 }
346}
347
348/// Automatically decrements a worker's job count when a job finishes (when
349/// this gets dropped).
350struct JobCountGuard(Arc<AtomicUsize>);
351
352impl Drop for JobCountGuard {
353 fn drop(&mut self) {
354 // Decrement the job count
355 let previous_value = self.0.fetch_sub(1, Ordering::SeqCst);
356 debug_assert!(previous_value >= 1);
357 }
358}
359
360/// Calls abort on the handle when dropped.
361struct AbortGuard(AbortHandle);
362
363impl Drop for AbortGuard {
364 fn drop(&mut self) {
365 self.0.abort();
366 }
367}
368
369type PinnedFutureSpawner = Box<dyn FnOnce() + Send + 'static>;
370
371struct LocalWorkerHandle {
372 runtime_handle: tokio::runtime::Handle,
373 spawner: UnboundedSender<PinnedFutureSpawner>,
374 task_count: Arc<AtomicUsize>,
375}
376
377impl LocalWorkerHandle {
378 /// Create a new worker for executing pinned tasks
379 fn new_worker() -> LocalWorkerHandle {
380 let (sender, receiver) = unbounded_channel();
381 let (handle_sender, handle_receiver) = std::sync::mpsc::channel();
382
383 let task_count = Arc::new(AtomicUsize::new(0));
384 let task_count_clone = Arc::clone(&task_count);
385
386 std::thread::spawn(|| Self::run(handle_sender, receiver, task_count_clone));
387
388 let runtime_handle = handle_receiver
389 .recv()
390 .expect("Failed to recv local runtime init result")
391 .expect("Failed to start local runtime");
392
393 LocalWorkerHandle {
394 runtime_handle,
395 spawner: sender,
396 task_count,
397 }
398 }
399
400 fn run(
401 handle_sender: std::sync::mpsc::Sender<std::io::Result<tokio::runtime::Handle>>,
402 mut task_receiver: UnboundedReceiver<PinnedFutureSpawner>,
403 task_count: Arc<AtomicUsize>,
404 ) {
405 let runtime = match tokio::runtime::LocalRuntime::new() {
406 Ok(runtime) => runtime,
407 Err(err) => {
408 let _ = handle_sender.send(Err(err));
409 return;
410 }
411 };
412
413 let runtime_handle = runtime.handle().clone();
414
415 handle_sender
416 .send(Ok(runtime_handle))
417 .expect("Failed to send local runtime handle");
418 drop(handle_sender);
419
420 runtime.block_on(async {
421 while let Some(spawn_task) = task_receiver.recv().await {
422 // Calls spawn_local(future)
423 (spawn_task)();
424 }
425 });
426
427 // If there are any tasks on the runtime that has already completed,
428 // but whose output has not yet been reported, let that task complete.
429 //
430 // Since the task_count is decremented when the runtime task exits,
431 // reading that counter lets us know if any such tasks completed during
432 // the call to `block_on`.
433 let mut previous_task_count = task_count.load(Ordering::SeqCst);
434 loop {
435 // This call will also run tasks spawned on the runtime.
436 runtime.block_on(tokio::task::yield_now());
437 let new_task_count = task_count.load(Ordering::SeqCst);
438 if new_task_count == previous_task_count {
439 break;
440 } else {
441 previous_task_count = new_task_count;
442 }
443 }
444
445 // It's now no longer possible for a task on the local runtime
446 // associated with task that has completed. Drop both
447 // local runtime to let tasks on the runtime be cancelled if and
448 // only if they are still on the runtime.
449 drop(runtime);
450 }
451}