Skip to main content

starnix_core/task/
dynamic_thread_spawner.rs

1// Copyright 2022 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! The API for spawning dynamic kernel threads.
6//!
7//! If you want to run a closure on a kernel thread, check out [SpawnRequestBuilder] on
8//! how to start and configure tasks that run closures.
9
10use crate::execution::create_kernel_thread;
11use crate::task::{
12    CurrentTask, DelayedReleaser, LockedAndTask, Task, ThreadLockupDetector, WrappedFuture,
13    with_new_current_task,
14};
15use futures::TryFutureExt;
16use futures::channel::oneshot;
17use starnix_logging::{CATEGORY_STARNIX, log_debug, log_error};
18use starnix_sync::{DynamicThreadSpawnerLock, LockDepMutex, Locked, Unlocked};
19use starnix_task_command::TaskCommand;
20use starnix_types::ownership::release_after;
21use starnix_uapi::errno;
22use starnix_uapi::errors::Errno;
23use std::future::Future;
24use std::sync::mpsc::{SendError, SyncSender, TrySendError, sync_channel};
25use std::sync::{Arc, Weak};
26use std::thread::JoinHandle;
27
28type BoxedClosure = Box<dyn FnOnce(&mut Locked<Unlocked>, &CurrentTask) -> () + Send + 'static>;
29
30const DEFAULT_THREAD_ROLE: &str = "fuchsia.starnix.fair.16";
31
32/// A builder for configuring new tasks to spawn.
33///
34/// The builder allows us to set up several different flavors of possible
35/// tasks to spawn, with a menu of options as follows:
36///
37/// - A task may or may not have a name.
38/// - A task may or may not have an assigned scheduler role.
39/// - A task may be a sync, or an async closure.
40/// - A task may return a result, or may not return a result.
41/// - The task's result can be collected synchronously, asynchronously, or not collected at all.
42///
43/// Note that these parameters are not perfectly orthogonal. For example, a task spawned from an
44/// async closure can not return a value asynchronously. (This is not a limitation of the approach,
45/// rather it's the API that we explicitly use today.) Also, some parameter combinations do not
46/// make sense, for example a spawn request can not have both a sync and an async closure to run.
47///
48/// The builder API is designed in a way that only allows chaining the configuration options which
49/// are valid at that point in the configuration process. Invalid option combinations are
50/// compile-time(!) errors. It will only allow creating a [SpawnRequest] if enough parameters have
51/// been passed such that there is enough information to create a request. It will not allow
52/// passing conflicting parameters: for example, if you already passed one synchronous closure, it
53/// is impossible to pass another closure and have the code compile without errors.
54///
55/// ## Usage
56///
57/// Call [SpawnRequestBuilder::new()] to start building. Refer to the unit tests in this module for
58/// usage examples.
59pub struct SpawnRequestBuilder<C: ClosureKind> {
60    debug_name: &'static str,
61    role: Option<&'static str>,
62    closure_kind: C,
63}
64
65/// You can only create an empty request builder.
66impl SpawnRequestBuilder<ClosureNone> {
67    /// Creates a new spawn request builder.
68    pub fn new() -> Self {
69        Self { role: None, closure_kind: ClosureNone {}, debug_name: "kthreadd" }
70    }
71}
72
73/// You can call these at any point in the builder's lifecycle.
74impl<C: ClosureKind> SpawnRequestBuilder<C> {
75    /// Set a role to apply to the thread that will run your closure.
76    pub fn with_role(self, role: &'static str) -> Self {
77        Self { role: Some(role), ..self }
78    }
79
80    /// Set a task name to apply to the thread that will run your closure.
81    pub fn with_debug_name(self, debug_name: &'static str) -> Self {
82        Self { debug_name, ..self }
83    }
84}
85
86/// You can call these only if you have not provided a closure yet.
87impl SpawnRequestBuilder<ClosureNone> {
88    /// Provides the closure that the spawner will run.
89    pub fn with_sync_closure<F, T>(
90        self,
91        f: F,
92    ) -> SpawnRequestBuilder<impl FnOnce(&mut Locked<Unlocked>, &CurrentTask) -> T + Send + 'static>
93    where
94        T: Send + 'static,
95        F: FnOnce(&mut Locked<Unlocked>, &CurrentTask) -> T + Send + 'static,
96    {
97        let SpawnRequestBuilder { role, closure_kind: _, debug_name } = self;
98        SpawnRequestBuilder { role, closure_kind: f, debug_name }
99    }
100
101    /// Provides the closure that the spawner will run.
102    pub fn with_async_closure<F, T>(
103        self,
104        f: F,
105    ) -> SpawnRequestBuilder<impl FnOnce(&mut Locked<Unlocked>, &CurrentTask) -> T + Send + 'static>
106    where
107        T: Send + 'static,
108        F: AsyncFnOnce(LockedAndTask<'_>) -> T + Send + 'static,
109    {
110        let sync_fn = async_to_sync(f, self.debug_name);
111        self.with_sync_closure(sync_fn)
112    }
113}
114
115/// A fully configured spawn request.
116pub struct SpawnRequest {
117    /// The closure to run.
118    closure: BoxedClosure,
119    /// A name to give to the task.
120    debug_name: &'static str,
121}
122
123impl<T, F> SpawnRequestBuilder<F>
124where
125    T: Send + 'static,
126    F: FnOnce(&mut Locked<Unlocked>, &CurrentTask) -> T + Send + 'static,
127{
128    /// Build a spawn request.
129    pub fn build(self) -> SpawnRequest {
130        let Self { role, closure_kind, debug_name } = self;
131        let closure = closure_kind;
132        let closure = maybe_apply_role(role, closure);
133        let closure = Box::new(move |locked: &mut Locked<Unlocked>, current_task: &CurrentTask| {
134            fuchsia_trace::duration!(CATEGORY_STARNIX, debug_name);
135            let _ = closure(locked, current_task);
136        });
137        SpawnRequest { closure, debug_name }
138    }
139
140    /// Like [build], but allows receiving a result synchronously.
141    /// Do not forget to submit the spawn request to a spawner.
142    ///
143    /// Example:
144    ///
145    /// ```
146    /// let (result_fn, request) = /*...*/ .build_with_sync_result();
147    /// // spawn `request`
148    /// let result = result_fn();
149    /// ```
150    pub fn build_with_sync_result(self) -> (impl FnOnce() -> Result<T, Errno>, SpawnRequest) {
151        let Self { role, closure_kind, debug_name } = self;
152        let closure = closure_kind;
153        let (sender, receiver) = sync_channel::<T>(0);
154        let result_fn = move || {
155            receiver.recv().map_err(|err| errno!(EINTR, format!("while receiving: {err:?}")))
156        };
157        let closure = maybe_apply_role(role, closure);
158        let closure = Box::new(move |locked: &mut Locked<Unlocked>, current_task: &CurrentTask| {
159            fuchsia_trace::duration!(CATEGORY_STARNIX, debug_name);
160            let _ = sender.send(closure(locked, current_task));
161        });
162        (result_fn, SpawnRequest { closure, debug_name })
163    }
164
165    /// Like [build], but allows receiving a result as a future.
166    /// Do not forget to submit the spawn request to a spawner.
167    ///
168    /// Example:
169    ///
170    /// ```
171    /// let (result_fut, request) = /*...*/ .build_with_async_result();
172    /// // spawn `request`
173    /// let result = result_fut.await;
174    /// ```
175    pub fn build_with_async_result(self) -> (impl Future<Output = Result<T, Errno>>, SpawnRequest) {
176        let Self { role, closure_kind, debug_name } = self;
177        let closure = closure_kind;
178        let (sender_async, result_fut) = oneshot::channel::<T>();
179        let maybe_with_role = maybe_apply_role(role, closure);
180        let repackaged =
181            Box::new(move |locked: &mut Locked<Unlocked>, current_task: &CurrentTask| {
182                fuchsia_trace::duration!(CATEGORY_STARNIX, debug_name);
183                let result = maybe_with_role(locked, current_task);
184                let _ = sender_async.send(result);
185            });
186        let result_fut =
187            result_fut.map_err(|err| errno!(EINTR, format!("while receiving async: {err:?}")));
188        (result_fut, SpawnRequest { closure: repackaged, debug_name })
189    }
190}
191
192/// A thread pool that immediately execute any new work sent to it and keep a maximum number of
193/// idle threads.
194#[derive(Debug)]
195pub struct DynamicThreadSpawner {
196    state: Arc<LockDepMutex<DynamicThreadSpawnerState, DynamicThreadSpawnerLock>>,
197    /// The weak system task to create the kernel thread associated with each thread.
198    system_task: Weak<Task>,
199    /// A persistent thread that is used to create new thread. This ensures that threads are
200    /// created from the initial starnix process and are not tied to a specific task.
201    persistent_thread: RunningThread,
202}
203
204/// Wrap a closure with a thread role assignment, if one is available.
205fn maybe_apply_role<R, F>(
206    role: Option<&'static str>,
207    f: F,
208) -> impl FnOnce(&mut Locked<Unlocked>, &CurrentTask) -> R + Send + 'static
209where
210    F: FnOnce(&mut Locked<Unlocked>, &CurrentTask) -> R + Send + 'static,
211{
212    move |locked, current_task| {
213        if let Some(role) = role {
214            if let Err(e) = fuchsia_scheduler::set_role_for_this_thread(role) {
215                log_debug!(e:%; "failed to set kthread role");
216            }
217            let result = f(locked, current_task);
218            if let Err(e) = fuchsia_scheduler::set_role_for_this_thread(DEFAULT_THREAD_ROLE) {
219                log_debug!(e:%; "failed to reset kthread role to default priority");
220            }
221            result
222        } else {
223            f(locked, current_task)
224        }
225    }
226}
227
228/// Convert async closure to sync closure that can be submitted to the spawner.
229fn async_to_sync<T, F>(
230    f: F,
231    name: &'static str,
232) -> impl FnOnce(&mut Locked<Unlocked>, &CurrentTask) -> T + Send + 'static
233where
234    T: Send + 'static,
235    F: AsyncFnOnce(LockedAndTask<'_>) -> T + Send + 'static,
236{
237    move |locked, current_task| {
238        let mut exec = fuchsia_async::LocalExecutor::default();
239        let locked_and_task = LockedAndTask::new(locked, current_task);
240
241        let locked_and_task_clone = locked_and_task.clone();
242        let wrapped_future = WrappedSpawnedFuture::new(
243            locked_and_task,
244            ThreadLockupDetector::track_future(f(locked_and_task_clone)),
245            name,
246        );
247        let _waiting_guard = ThreadLockupDetector::pause_tracking();
248        exec.run_singlethreaded(wrapped_future)
249    }
250}
251
252/// Denotes whether a closure has been provided. A request can not be
253/// built at all without a closure.
254///
255/// See [SpawnRequestBuilder] for usage details.
256pub trait ClosureKind {}
257
258/// A builder type state where no closure has been provided yet.
259///
260/// See [SpawnRequestBuilder] for usage details.
261pub struct ClosureNone {}
262impl ClosureKind for ClosureNone {}
263
264/// A builder type state where a closure has been provided.
265/// See [SpawnRequestBuilder] for usage details.
266impl<T: Send + 'static, FN: FnOnce(&mut Locked<Unlocked>, &CurrentTask) -> T + Send + 'static>
267    ClosureKind for FN
268{
269}
270
271#[derive(Debug)]
272struct DynamicThreadSpawnerState {
273    threads: Vec<RunningThread>,
274    idle_threads: u8,
275    max_idle_threads: u8,
276}
277
278impl DynamicThreadSpawner {
279    pub fn new(
280        max_idle_threads: u8,
281        system_task: Weak<Task>,
282        debug_name: impl Into<String>,
283    ) -> Self {
284        let persistent_thread =
285            RunningThread::new_persistent(system_task.clone(), debug_name.into());
286        Self {
287            state: Arc::new(
288                DynamicThreadSpawnerState { max_idle_threads, idle_threads: 0, threads: vec![] }
289                    .into(),
290            ),
291            system_task,
292            persistent_thread,
293        }
294    }
295
296    /// Run a given closure on a thread based on the provided [SpawnRequest].
297    ///
298    /// Use [SpawnRequestBuilder::new()] to start configuring a [SpawnRequest].
299    ///
300    /// This method will use an idle thread in the pool if one is available, otherwise it will
301    /// start a new thread. When this method returns, it is guaranteed that a thread is
302    /// responsible to start running the closure.
303    pub fn spawn_from_request(&self, spawn_request: SpawnRequest) {
304        // Check whether a thread already exists to handle the request.
305        let mut function: BoxedClosure = spawn_request.closure;
306        let mut state = self.state.lock();
307        if state.idle_threads > 0 {
308            let mut i = 0;
309            while i < state.threads.len() {
310                // Increases `i` immediately, so that it can be decreased it the thread must be
311                // dropped.
312                let thread_index = i;
313                i += 1;
314                match state.threads[thread_index].try_dispatch(function) {
315                    Ok(_) => {
316                        // The dispatch succeeded.
317                        state.idle_threads -= 1;
318                        return;
319                    }
320                    Err(TrySendError::Full(f)) => {
321                        // The thread is busy.
322                        function = f;
323                    }
324                    Err(TrySendError::Disconnected(f)) => {
325                        // The receiver is disconnected, it means the thread has terminated, drop it.
326                        state.idle_threads -= 1;
327                        state.threads.remove(thread_index);
328                        i -= 1;
329                        function = f;
330                    }
331                }
332            }
333        }
334
335        // A new thread must be created. It needs to be done from the persistent thread.
336        let (sender, receiver) = sync_channel::<RunningThread>(0);
337        let dispatch_function: BoxedClosure = Box::new({
338            let state = self.state.clone();
339            let system_task = self.system_task.clone();
340            move |_, _| {
341                sender
342                    .send(RunningThread::new(
343                        state,
344                        system_task,
345                        spawn_request.debug_name.to_string(),
346                        function,
347                    ))
348                    .expect("receiver must not be dropped");
349            }
350        });
351        self.persistent_thread
352            .dispatch(dispatch_function)
353            .expect("persistent thread should not have ended.");
354        state.threads.push(receiver.recv().expect("persistent thread should not have ended."));
355    }
356}
357
358type WrappedSpawnedFuture<'a, F> = WrappedFuture<F, LockedAndTask<'a>>;
359
360impl<'a, F: 'a> WrappedSpawnedFuture<'a, F> {
361    fn new(locked_and_task: LockedAndTask<'a>, fut: F, name: &'static str) -> Self {
362        Self::new_with_cleaner(locked_and_task, trigger_delayed_releaser, fut, name)
363    }
364}
365
366fn trigger_delayed_releaser(locked_and_task: LockedAndTask<'_>) {
367    locked_and_task.current_task().trigger_delayed_releaser(&mut locked_and_task.unlocked());
368}
369
370#[derive(Debug)]
371struct RunningThread {
372    thread: Option<JoinHandle<()>>,
373    sender: Option<SyncSender<BoxedClosure>>,
374}
375
376impl RunningThread {
377    fn new(
378        state: Arc<LockDepMutex<DynamicThreadSpawnerState, DynamicThreadSpawnerLock>>,
379        system_task: Weak<Task>,
380        debug_task_name: String,
381        f: BoxedClosure,
382    ) -> Self {
383        let (sender, receiver) = sync_channel::<BoxedClosure>(0);
384        let thread = Some(
385            std::thread::Builder::new()
386                .name("kthread-dynamic-worker".to_string())
387                .spawn(move || {
388                    // It's ok to create a new lock context here, since we are on a new thread.
389                    #[allow(
390                        clippy::undocumented_unsafe_blocks,
391                        reason = "Force documented unsafe blocks in Starnix"
392                    )]
393                    let locked = unsafe { Unlocked::new() };
394                    let result = with_new_current_task(
395                        locked,
396                        &system_task,
397                        debug_task_name,
398                        |locked, current_task| {
399                            while let Ok(f) = receiver.recv() {
400                                let _guard = ThreadLockupDetector::track();
401                                f(locked, &current_task);
402                                // Apply any delayed releasers.
403                                current_task.trigger_delayed_releaser(locked);
404                                let mut state = state.lock();
405                                state.idle_threads += 1;
406                                if state.idle_threads > state.max_idle_threads {
407                                    // If the number of idle thread is greater than the max, the
408                                    // thread terminates.  This disconnects the receiver, which will
409                                    // ensure that the thread will be joined and remove from the list
410                                    // of available threads the next time the pool tries to use it.
411                                    return;
412                                }
413                            }
414                        },
415                    );
416                    if let Err(e) = result {
417                        log_error!("Unable to create a kernel thread: {e:?}");
418                    }
419                })
420                .expect("able to create threads"),
421        );
422        let result = Self { thread, sender: Some(sender) };
423        // The dispatch cannot fail because the thread can only finish after having executed at
424        // least one task, and this is the first task ever dispatched to it.
425        result
426            .sender
427            .as_ref()
428            .expect("sender should never be None")
429            .send(f)
430            .expect("Dispatch cannot fail");
431        result
432    }
433
434    fn new_persistent(system_task: Weak<Task>, task_name: String) -> Self {
435        // The persistent thread doesn't need to do any rendez-vous when received task.
436        let (sender, receiver) = sync_channel::<BoxedClosure>(20);
437        let thread = Some(
438            std::thread::Builder::new()
439                .name("kthread-persistent-worker".to_string())
440                .spawn(move || {
441                    // It's ok to create a new lock context here, since we are on a new thread.
442                    #[allow(
443                        clippy::undocumented_unsafe_blocks,
444                        reason = "Force documented unsafe blocks in Starnix"
445                    )]
446                    let locked = unsafe { Unlocked::new() };
447                    let current_task = {
448                        let Some(system_task) = system_task.upgrade() else {
449                            return;
450                        };
451                        match create_kernel_thread(
452                            locked,
453                            &system_task,
454                            TaskCommand::new(task_name.as_bytes()),
455                        ) {
456                            Ok(task) => task,
457                            Err(e) => {
458                                log_error!("Unable to create a kernel thread: {e:?}");
459                                return;
460                            }
461                        }
462                    };
463                    release_after!(current_task, locked, {
464                        while let Ok(f) = receiver.recv() {
465                            let _guard = ThreadLockupDetector::track();
466                            f(locked, &current_task);
467
468                            // Apply any delayed releasers.
469                            current_task.trigger_delayed_releaser(locked);
470                        }
471                    });
472
473                    // Ensure that no releasables are registered after this point as we unwind the stack.
474                    DelayedReleaser::finalize();
475                })
476                .expect("able to create threads"),
477        );
478        Self { thread, sender: Some(sender) }
479    }
480
481    fn try_dispatch(&self, f: BoxedClosure) -> Result<(), TrySendError<BoxedClosure>> {
482        self.sender.as_ref().expect("sender should never be None").try_send(f)
483    }
484
485    fn dispatch(&self, f: BoxedClosure) -> Result<(), SendError<BoxedClosure>> {
486        self.sender.as_ref().expect("sender should never be None").send(f)
487    }
488}
489
490impl Drop for RunningThread {
491    fn drop(&mut self) {
492        self.sender = None;
493        match self.thread.take() {
494            Some(thread) => thread.join().expect("Thread should join."),
495            _ => panic!("Thread should never be None"),
496        };
497    }
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503    use crate::testing::spawn_kernel_and_run;
504
505    #[fuchsia::test]
506    async fn run_simple_task() {
507        spawn_kernel_and_run(async |_, current_task| {
508            let spawner = DynamicThreadSpawner::new(2, current_task.weak_task(), "kthreadd");
509            // Type decorations are needed sometimes to avoid "closure type is
510            // not general enough" error.
511            let closure = move |_: &mut Locked<Unlocked>, _: &CurrentTask| {};
512            let req = SpawnRequestBuilder::new().with_sync_closure(closure).build();
513            spawner.spawn_from_request(req);
514        })
515        .await;
516    }
517
518    #[fuchsia::test]
519    async fn run_10_tasks() {
520        spawn_kernel_and_run(async |_, current_task| {
521            let spawner = DynamicThreadSpawner::new(2, current_task.weak_task(), "kthreadd");
522            for _ in 0..10 {
523                let closure = move |_: &mut Locked<Unlocked>, _: &CurrentTask| {};
524                let opts = SpawnRequestBuilder::new().with_sync_closure(closure).build();
525                spawner.spawn_from_request(opts);
526            }
527        })
528        .await;
529    }
530
531    #[fuchsia::test]
532    async fn blocking_task_do_not_prevent_further_processing() {
533        spawn_kernel_and_run(async |_, current_task| {
534            let spawner = DynamicThreadSpawner::new(1, current_task.weak_task(), "kthreadd");
535
536            let pair = Arc::new((fuchsia_sync::Mutex::new(false), fuchsia_sync::Condvar::new()));
537            for _ in 0..10 {
538                let pair2 = Arc::clone(&pair);
539                let closure = move |_: &mut Locked<Unlocked>, _: &CurrentTask| {
540                    let (lock, cvar) = &*pair2;
541                    let mut cont = lock.lock();
542                    while !*cont {
543                        cvar.wait(&mut cont);
544                    }
545                };
546                let req = SpawnRequestBuilder::new().with_sync_closure(closure).build();
547                spawner.spawn_from_request(req);
548            }
549
550            let closure = move |_: &mut Locked<Unlocked>, _: &CurrentTask| {
551                let (lock, cvar) = &*pair;
552                let mut cont = lock.lock();
553                *cont = true;
554                cvar.notify_all();
555            };
556
557            let (result, req) =
558                SpawnRequestBuilder::new().with_sync_closure(closure).build_with_sync_result();
559            spawner.spawn_from_request(req);
560
561            assert_eq!(result(), Ok(()));
562        })
563        .await;
564    }
565
566    #[fuchsia::test]
567    async fn run_spawn_and_get_result() {
568        spawn_kernel_and_run(async |_, current_task| {
569            let spawner = DynamicThreadSpawner::new(2, current_task.weak_task(), "kthreadd");
570
571            let (result, req) =
572                SpawnRequestBuilder::new().with_sync_closure(|_, _| 3).build_with_sync_result();
573            spawner.spawn_from_request(req);
574            assert_eq!(result(), Ok(3));
575        })
576        .await;
577    }
578
579    #[fuchsia::test]
580    async fn test_spawn_async() {
581        spawn_kernel_and_run(async |_, current_task| {
582            let spawner = DynamicThreadSpawner::new(2, current_task.weak_task(), "kthreadd");
583
584            // The closure free variables must be decorated with their respective types,
585            // the rust compiler gets confused otherwise and is unable to infer the correct
586            // lifetimes. Interestingly, adding your own lifetimes here does *not* help.
587            let closure = move |locked: &mut Locked<Unlocked>, current_task: &CurrentTask| {
588                let mut exec = fuchsia_async::LocalExecutor::default();
589                let locked_and_task = LockedAndTask::new(locked, current_task);
590                let fut = async {};
591                let wrapped_future = WrappedSpawnedFuture::new(locked_and_task, fut, "test-async");
592                exec.run_singlethreaded(wrapped_future);
593            };
594            let req = SpawnRequestBuilder::new().with_sync_closure(closure).build();
595            spawner.spawn_from_request(req);
596        })
597        .await;
598    }
599
600    #[fuchsia::test]
601    async fn test_spawn_async_closure() {
602        spawn_kernel_and_run(async |_, current_task| {
603            let spawner = DynamicThreadSpawner::new(2, current_task.weak_task(), "kthreadd");
604            let fut = async |_: LockedAndTask<'_>| 42;
605            let (result, req) =
606                SpawnRequestBuilder::new().with_async_closure(fut).build_with_sync_result();
607            spawner.spawn_from_request(req);
608            assert_eq!(result(), Ok(42));
609        })
610        .await;
611    }
612
613    #[fuchsia::test]
614    async fn test_spawn_sync_to_async_result() {
615        spawn_kernel_and_run(async |_, current_task| {
616            let spawner = DynamicThreadSpawner::new(2, current_task.weak_task(), "kthreadd");
617            let fut = async |_: LockedAndTask<'_>| 42;
618            let (result, req) =
619                SpawnRequestBuilder::new().with_async_closure(fut).build_with_sync_result();
620
621            let fut2 = async move |_: LockedAndTask<'_>| result().unwrap();
622            let (result2, req2) =
623                SpawnRequestBuilder::new().with_async_closure(fut2).build_with_sync_result();
624            spawner.spawn_from_request(req2);
625            spawner.spawn_from_request(req);
626            assert_eq!(result2(), Ok(42));
627        })
628        .await;
629    }
630
631    #[fuchsia::test]
632    async fn test_spawn_async_to_async_result() {
633        spawn_kernel_and_run(async |_, current_task| {
634            let spawner = DynamicThreadSpawner::new(2, current_task.weak_task(), "kthreadd");
635            let fut = async |_: LockedAndTask<'_>| 42;
636            let (result_fut, req) =
637                SpawnRequestBuilder::new().with_async_closure(fut).build_with_async_result();
638
639            let fut2 = async move |_: LockedAndTask<'_>| result_fut.await.unwrap();
640            let (result2, req2) =
641                SpawnRequestBuilder::new().with_async_closure(fut2).build_with_sync_result();
642            spawner.spawn_from_request(req2);
643            spawner.spawn_from_request(req);
644            assert_eq!(result2(), Ok(42));
645        })
646        .await;
647    }
648}