madsim_real_tokio/task/
join_set.rs

1//! A collection of tasks spawned on a Tokio runtime.
2//!
3//! This module provides the [`JoinSet`] type, a collection which stores a set
4//! of spawned tasks and allows asynchronously awaiting the output of those
5//! tasks as they complete. See the documentation for the [`JoinSet`] type for
6//! details.
7use std::fmt;
8use std::future::Future;
9use std::pin::Pin;
10use std::task::{Context, Poll};
11
12use crate::runtime::Handle;
13#[cfg(tokio_unstable)]
14use crate::task::Id;
15use crate::task::{unconstrained, AbortHandle, JoinError, JoinHandle, LocalSet};
16use crate::util::IdleNotifiedSet;
17
18/// A collection of tasks spawned on a Tokio runtime.
19///
20/// A `JoinSet` can be used to await the completion of some or all of the tasks
21/// in the set. The set is not ordered, and the tasks will be returned in the
22/// order they complete.
23///
24/// All of the tasks must have the same return type `T`.
25///
26/// When the `JoinSet` is dropped, all tasks in the `JoinSet` are immediately aborted.
27///
28/// # Examples
29///
30/// Spawn multiple tasks and wait for them.
31///
32/// ```
33/// use tokio::task::JoinSet;
34///
35/// #[tokio::main]
36/// async fn main() {
37///     let mut set = JoinSet::new();
38///
39///     for i in 0..10 {
40///         set.spawn(async move { i });
41///     }
42///
43///     let mut seen = [false; 10];
44///     while let Some(res) = set.join_next().await {
45///         let idx = res.unwrap();
46///         seen[idx] = true;
47///     }
48///
49///     for i in 0..10 {
50///         assert!(seen[i]);
51///     }
52/// }
53/// ```
54#[cfg_attr(docsrs, doc(cfg(feature = "rt")))]
55pub struct JoinSet<T> {
56    inner: IdleNotifiedSet<JoinHandle<T>>,
57}
58
59/// A variant of [`task::Builder`] that spawns tasks on a [`JoinSet`] rather
60/// than on the current default runtime.
61///
62/// [`task::Builder`]: crate::task::Builder
63#[cfg(all(tokio_unstable, feature = "tracing"))]
64#[cfg_attr(docsrs, doc(cfg(all(tokio_unstable, feature = "tracing"))))]
65#[must_use = "builders do nothing unless used to spawn a task"]
66pub struct Builder<'a, T> {
67    joinset: &'a mut JoinSet<T>,
68    builder: super::Builder<'a>,
69}
70
71impl<T> JoinSet<T> {
72    /// Create a new `JoinSet`.
73    pub fn new() -> Self {
74        Self {
75            inner: IdleNotifiedSet::new(),
76        }
77    }
78
79    /// Returns the number of tasks currently in the `JoinSet`.
80    pub fn len(&self) -> usize {
81        self.inner.len()
82    }
83
84    /// Returns whether the `JoinSet` is empty.
85    pub fn is_empty(&self) -> bool {
86        self.inner.is_empty()
87    }
88}
89
90impl<T: 'static> JoinSet<T> {
91    /// Returns a [`Builder`] that can be used to configure a task prior to
92    /// spawning it on this `JoinSet`.
93    ///
94    /// # Examples
95    ///
96    /// ```
97    /// use tokio::task::JoinSet;
98    ///
99    /// #[tokio::main]
100    /// async fn main() -> std::io::Result<()> {
101    ///     let mut set = JoinSet::new();
102    ///
103    ///     // Use the builder to configure a task's name before spawning it.
104    ///     set.build_task()
105    ///         .name("my_task")
106    ///         .spawn(async { /* ... */ })?;
107    ///
108    ///     Ok(())
109    /// }
110    /// ```
111    #[cfg(all(tokio_unstable, feature = "tracing"))]
112    #[cfg_attr(docsrs, doc(cfg(all(tokio_unstable, feature = "tracing"))))]
113    pub fn build_task(&mut self) -> Builder<'_, T> {
114        Builder {
115            builder: super::Builder::new(),
116            joinset: self,
117        }
118    }
119
120    /// Spawn the provided task on the `JoinSet`, returning an [`AbortHandle`]
121    /// that can be used to remotely cancel the task.
122    ///
123    /// The provided future will start running in the background immediately
124    /// when this method is called, even if you don't await anything on this
125    /// `JoinSet`.
126    ///
127    /// # Panics
128    ///
129    /// This method panics if called outside of a Tokio runtime.
130    ///
131    /// [`AbortHandle`]: crate::task::AbortHandle
132    #[track_caller]
133    pub fn spawn<F>(&mut self, task: F) -> AbortHandle
134    where
135        F: Future<Output = T>,
136        F: Send + 'static,
137        T: Send,
138    {
139        self.insert(crate::spawn(task))
140    }
141
142    /// Spawn the provided task on the provided runtime and store it in this
143    /// `JoinSet` returning an [`AbortHandle`] that can be used to remotely
144    /// cancel the task.
145    ///
146    /// The provided future will start running in the background immediately
147    /// when this method is called, even if you don't await anything on this
148    /// `JoinSet`.
149    ///
150    /// [`AbortHandle`]: crate::task::AbortHandle
151    #[track_caller]
152    pub fn spawn_on<F>(&mut self, task: F, handle: &Handle) -> AbortHandle
153    where
154        F: Future<Output = T>,
155        F: Send + 'static,
156        T: Send,
157    {
158        self.insert(handle.spawn(task))
159    }
160
161    /// Spawn the provided task on the current [`LocalSet`] and store it in this
162    /// `JoinSet`, returning an [`AbortHandle`] that can be used to remotely
163    /// cancel the task.
164    ///
165    /// The provided future will start running in the background immediately
166    /// when this method is called, even if you don't await anything on this
167    /// `JoinSet`.
168    ///
169    /// # Panics
170    ///
171    /// This method panics if it is called outside of a `LocalSet`.
172    ///
173    /// [`LocalSet`]: crate::task::LocalSet
174    /// [`AbortHandle`]: crate::task::AbortHandle
175    #[track_caller]
176    pub fn spawn_local<F>(&mut self, task: F) -> AbortHandle
177    where
178        F: Future<Output = T>,
179        F: 'static,
180    {
181        self.insert(crate::task::spawn_local(task))
182    }
183
184    /// Spawn the provided task on the provided [`LocalSet`] and store it in
185    /// this `JoinSet`, returning an [`AbortHandle`] that can be used to
186    /// remotely cancel the task.
187    ///
188    /// Unlike the [`spawn_local`] method, this method may be used to spawn local
189    /// tasks on a `LocalSet` that is _not_ currently running. The provided
190    /// future will start running whenever the `LocalSet` is next started.
191    ///
192    /// [`LocalSet`]: crate::task::LocalSet
193    /// [`AbortHandle`]: crate::task::AbortHandle
194    /// [`spawn_local`]: Self::spawn_local
195    #[track_caller]
196    pub fn spawn_local_on<F>(&mut self, task: F, local_set: &LocalSet) -> AbortHandle
197    where
198        F: Future<Output = T>,
199        F: 'static,
200    {
201        self.insert(local_set.spawn_local(task))
202    }
203
204    /// Spawn the blocking code on the blocking threadpool and store
205    /// it in this `JoinSet`, returning an [`AbortHandle`] that can be
206    /// used to remotely cancel the task.
207    ///
208    /// # Examples
209    ///
210    /// Spawn multiple blocking tasks and wait for them.
211    ///
212    /// ```
213    /// use tokio::task::JoinSet;
214    ///
215    /// #[tokio::main]
216    /// async fn main() {
217    ///     let mut set = JoinSet::new();
218    ///
219    ///     for i in 0..10 {
220    ///         set.spawn_blocking(move || { i });
221    ///     }
222    ///
223    ///     let mut seen = [false; 10];
224    ///     while let Some(res) = set.join_next().await {
225    ///         let idx = res.unwrap();
226    ///         seen[idx] = true;
227    ///     }
228    ///
229    ///     for i in 0..10 {
230    ///         assert!(seen[i]);
231    ///     }
232    /// }
233    /// ```
234    ///
235    /// # Panics
236    ///
237    /// This method panics if called outside of a Tokio runtime.
238    ///
239    /// [`AbortHandle`]: crate::task::AbortHandle
240    #[track_caller]
241    pub fn spawn_blocking<F>(&mut self, f: F) -> AbortHandle
242    where
243        F: FnOnce() -> T,
244        F: Send + 'static,
245        T: Send,
246    {
247        self.insert(crate::runtime::spawn_blocking(f))
248    }
249
250    /// Spawn the blocking code on the blocking threadpool of the
251    /// provided runtime and store it in this `JoinSet`, returning an
252    /// [`AbortHandle`] that can be used to remotely cancel the task.
253    ///
254    /// [`AbortHandle`]: crate::task::AbortHandle
255    #[track_caller]
256    pub fn spawn_blocking_on<F>(&mut self, f: F, handle: &Handle) -> AbortHandle
257    where
258        F: FnOnce() -> T,
259        F: Send + 'static,
260        T: Send,
261    {
262        self.insert(handle.spawn_blocking(f))
263    }
264
265    fn insert(&mut self, jh: JoinHandle<T>) -> AbortHandle {
266        let abort = jh.abort_handle();
267        let mut entry = self.inner.insert_idle(jh);
268
269        // Set the waker that is notified when the task completes.
270        entry.with_value_and_context(|jh, ctx| jh.set_join_waker(ctx.waker()));
271        abort
272    }
273
274    /// Waits until one of the tasks in the set completes and returns its output.
275    ///
276    /// Returns `None` if the set is empty.
277    ///
278    /// # Cancel Safety
279    ///
280    /// This method is cancel safe. If `join_next` is used as the event in a `tokio::select!`
281    /// statement and some other branch completes first, it is guaranteed that no tasks were
282    /// removed from this `JoinSet`.
283    pub async fn join_next(&mut self) -> Option<Result<T, JoinError>> {
284        crate::future::poll_fn(|cx| self.poll_join_next(cx)).await
285    }
286
287    /// Waits until one of the tasks in the set completes and returns its
288    /// output, along with the [task ID] of the completed task.
289    ///
290    /// Returns `None` if the set is empty.
291    ///
292    /// When this method returns an error, then the id of the task that failed can be accessed
293    /// using the [`JoinError::id`] method.
294    ///
295    /// # Cancel Safety
296    ///
297    /// This method is cancel safe. If `join_next_with_id` is used as the event in a `tokio::select!`
298    /// statement and some other branch completes first, it is guaranteed that no tasks were
299    /// removed from this `JoinSet`.
300    ///
301    /// [task ID]: crate::task::Id
302    /// [`JoinError::id`]: fn@crate::task::JoinError::id
303    #[cfg(tokio_unstable)]
304    #[cfg_attr(docsrs, doc(cfg(tokio_unstable)))]
305    pub async fn join_next_with_id(&mut self) -> Option<Result<(Id, T), JoinError>> {
306        crate::future::poll_fn(|cx| self.poll_join_next_with_id(cx)).await
307    }
308
309    /// Tries to join one of the tasks in the set that has completed and return its output.
310    ///
311    /// Returns `None` if the set is empty.    
312    pub fn try_join_next(&mut self) -> Option<Result<T, JoinError>> {
313        // Loop over all notified `JoinHandle`s to find one that's ready, or until none are left.
314        loop {
315            let mut entry = self.inner.try_pop_notified()?;
316
317            let res = entry.with_value_and_context(|jh, ctx| {
318                // Since this function is not async and cannot be forced to yield, we should
319                // disable budgeting when we want to check for the `JoinHandle` readiness.
320                Pin::new(&mut unconstrained(jh)).poll(ctx)
321            });
322
323            if let Poll::Ready(res) = res {
324                let _entry = entry.remove();
325
326                return Some(res);
327            }
328        }
329    }
330
331    /// Tries to join one of the tasks in the set that has completed and return its output,
332    /// along with the [task ID] of the completed task.
333    ///
334    /// Returns `None` if the set is empty.
335    ///
336    /// When this method returns an error, then the id of the task that failed can be accessed
337    /// using the [`JoinError::id`] method.
338    ///
339    /// [task ID]: crate::task::Id
340    /// [`JoinError::id`]: fn@crate::task::JoinError::id
341    #[cfg(tokio_unstable)]
342    #[cfg_attr(docsrs, doc(cfg(tokio_unstable)))]
343    pub fn try_join_next_with_id(&mut self) -> Option<Result<(Id, T), JoinError>> {
344        // Loop over all notified `JoinHandle`s to find one that's ready, or until none are left.
345        loop {
346            let mut entry = self.inner.try_pop_notified()?;
347
348            let res = entry.with_value_and_context(|jh, ctx| {
349                // Since this function is not async and cannot be forced to yield, we should
350                // disable budgeting when we want to check for the `JoinHandle` readiness.
351                Pin::new(&mut unconstrained(jh)).poll(ctx)
352            });
353
354            if let Poll::Ready(res) = res {
355                let entry = entry.remove();
356
357                return Some(res.map(|output| (entry.id(), output)));
358            }
359        }
360    }
361
362    /// Aborts all tasks and waits for them to finish shutting down.
363    ///
364    /// Calling this method is equivalent to calling [`abort_all`] and then calling [`join_next`] in
365    /// a loop until it returns `None`.
366    ///
367    /// This method ignores any panics in the tasks shutting down. When this call returns, the
368    /// `JoinSet` will be empty.
369    ///
370    /// [`abort_all`]: fn@Self::abort_all
371    /// [`join_next`]: fn@Self::join_next
372    pub async fn shutdown(&mut self) {
373        self.abort_all();
374        while self.join_next().await.is_some() {}
375    }
376
377    /// Aborts all tasks on this `JoinSet`.
378    ///
379    /// This does not remove the tasks from the `JoinSet`. To wait for the tasks to complete
380    /// cancellation, you should call `join_next` in a loop until the `JoinSet` is empty.
381    pub fn abort_all(&mut self) {
382        self.inner.for_each(|jh| jh.abort());
383    }
384
385    /// Removes all tasks from this `JoinSet` without aborting them.
386    ///
387    /// The tasks removed by this call will continue to run in the background even if the `JoinSet`
388    /// is dropped.
389    pub fn detach_all(&mut self) {
390        self.inner.drain(drop);
391    }
392
393    /// Polls for one of the tasks in the set to complete.
394    ///
395    /// If this returns `Poll::Ready(Some(_))`, then the task that completed is removed from the set.
396    ///
397    /// When the method returns `Poll::Pending`, the `Waker` in the provided `Context` is scheduled
398    /// to receive a wakeup when a task in the `JoinSet` completes. Note that on multiple calls to
399    /// `poll_join_next`, only the `Waker` from the `Context` passed to the most recent call is
400    /// scheduled to receive a wakeup.
401    ///
402    /// # Returns
403    ///
404    /// This function returns:
405    ///
406    ///  * `Poll::Pending` if the `JoinSet` is not empty but there is no task whose output is
407    ///     available right now.
408    ///  * `Poll::Ready(Some(Ok(value)))` if one of the tasks in this `JoinSet` has completed.
409    ///     The `value` is the return value of one of the tasks that completed.
410    ///  * `Poll::Ready(Some(Err(err)))` if one of the tasks in this `JoinSet` has panicked or been
411    ///     aborted. The `err` is the `JoinError` from the panicked/aborted task.
412    ///  * `Poll::Ready(None)` if the `JoinSet` is empty.
413    ///
414    /// Note that this method may return `Poll::Pending` even if one of the tasks has completed.
415    /// This can happen if the [coop budget] is reached.
416    ///
417    /// [coop budget]: crate::task#cooperative-scheduling
418    pub fn poll_join_next(&mut self, cx: &mut Context<'_>) -> Poll<Option<Result<T, JoinError>>> {
419        // The call to `pop_notified` moves the entry to the `idle` list. It is moved back to
420        // the `notified` list if the waker is notified in the `poll` call below.
421        let mut entry = match self.inner.pop_notified(cx.waker()) {
422            Some(entry) => entry,
423            None => {
424                if self.is_empty() {
425                    return Poll::Ready(None);
426                } else {
427                    // The waker was set by `pop_notified`.
428                    return Poll::Pending;
429                }
430            }
431        };
432
433        let res = entry.with_value_and_context(|jh, ctx| Pin::new(jh).poll(ctx));
434
435        if let Poll::Ready(res) = res {
436            let _entry = entry.remove();
437            Poll::Ready(Some(res))
438        } else {
439            // A JoinHandle generally won't emit a wakeup without being ready unless
440            // the coop limit has been reached. We yield to the executor in this
441            // case.
442            cx.waker().wake_by_ref();
443            Poll::Pending
444        }
445    }
446
447    /// Polls for one of the tasks in the set to complete.
448    ///
449    /// If this returns `Poll::Ready(Some(_))`, then the task that completed is removed from the set.
450    ///
451    /// When the method returns `Poll::Pending`, the `Waker` in the provided `Context` is scheduled
452    /// to receive a wakeup when a task in the `JoinSet` completes. Note that on multiple calls to
453    /// `poll_join_next`, only the `Waker` from the `Context` passed to the most recent call is
454    /// scheduled to receive a wakeup.
455    ///
456    /// # Returns
457    ///
458    /// This function returns:
459    ///
460    ///  * `Poll::Pending` if the `JoinSet` is not empty but there is no task whose output is
461    ///     available right now.
462    ///  * `Poll::Ready(Some(Ok((id, value))))` if one of the tasks in this `JoinSet` has completed.
463    ///     The `value` is the return value of one of the tasks that completed, and
464    ///    `id` is the [task ID] of that task.
465    ///  * `Poll::Ready(Some(Err(err)))` if one of the tasks in this `JoinSet` has panicked or been
466    ///     aborted. The `err` is the `JoinError` from the panicked/aborted task.
467    ///  * `Poll::Ready(None)` if the `JoinSet` is empty.
468    ///
469    /// Note that this method may return `Poll::Pending` even if one of the tasks has completed.
470    /// This can happen if the [coop budget] is reached.
471    ///
472    /// [coop budget]: crate::task#cooperative-scheduling
473    /// [task ID]: crate::task::Id
474    #[cfg(tokio_unstable)]
475    #[cfg_attr(docsrs, doc(cfg(tokio_unstable)))]
476    pub fn poll_join_next_with_id(
477        &mut self,
478        cx: &mut Context<'_>,
479    ) -> Poll<Option<Result<(Id, T), JoinError>>> {
480        // The call to `pop_notified` moves the entry to the `idle` list. It is moved back to
481        // the `notified` list if the waker is notified in the `poll` call below.
482        let mut entry = match self.inner.pop_notified(cx.waker()) {
483            Some(entry) => entry,
484            None => {
485                if self.is_empty() {
486                    return Poll::Ready(None);
487                } else {
488                    // The waker was set by `pop_notified`.
489                    return Poll::Pending;
490                }
491            }
492        };
493
494        let res = entry.with_value_and_context(|jh, ctx| Pin::new(jh).poll(ctx));
495
496        if let Poll::Ready(res) = res {
497            let entry = entry.remove();
498            // If the task succeeded, add the task ID to the output. Otherwise, the
499            // `JoinError` will already have the task's ID.
500            Poll::Ready(Some(res.map(|output| (entry.id(), output))))
501        } else {
502            // A JoinHandle generally won't emit a wakeup without being ready unless
503            // the coop limit has been reached. We yield to the executor in this
504            // case.
505            cx.waker().wake_by_ref();
506            Poll::Pending
507        }
508    }
509}
510
511impl<T> Drop for JoinSet<T> {
512    fn drop(&mut self) {
513        self.inner.drain(|join_handle| join_handle.abort());
514    }
515}
516
517impl<T> fmt::Debug for JoinSet<T> {
518    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
519        f.debug_struct("JoinSet").field("len", &self.len()).finish()
520    }
521}
522
523impl<T> Default for JoinSet<T> {
524    fn default() -> Self {
525        Self::new()
526    }
527}
528
529/// Collect an iterator of futures into a [`JoinSet`].
530///
531/// This is equivalent to calling [`JoinSet::spawn`] on each element of the iterator.
532///
533/// # Examples
534///
535/// The main example from [`JoinSet`]'s documentation can also be written using [`collect`]:
536///
537/// ```
538/// use tokio::task::JoinSet;
539///
540/// #[tokio::main]
541/// async fn main() {
542///     let mut set: JoinSet<_> = (0..10).map(|i| async move { i }).collect();
543///
544///     let mut seen = [false; 10];
545///     while let Some(res) = set.join_next().await {
546///         let idx = res.unwrap();
547///         seen[idx] = true;
548///     }
549///
550///     for i in 0..10 {
551///         assert!(seen[i]);
552///     }
553/// }
554/// ```
555///
556/// [`collect`]: std::iter::Iterator::collect
557impl<T, F> std::iter::FromIterator<F> for JoinSet<T>
558where
559    F: Future<Output = T>,
560    F: Send + 'static,
561    T: Send + 'static,
562{
563    fn from_iter<I: IntoIterator<Item = F>>(iter: I) -> Self {
564        let mut set = Self::new();
565        iter.into_iter().for_each(|task| {
566            set.spawn(task);
567        });
568        set
569    }
570}
571
572// === impl Builder ===
573
574#[cfg(all(tokio_unstable, feature = "tracing"))]
575#[cfg_attr(docsrs, doc(cfg(all(tokio_unstable, feature = "tracing"))))]
576impl<'a, T: 'static> Builder<'a, T> {
577    /// Assigns a name to the task which will be spawned.
578    pub fn name(self, name: &'a str) -> Self {
579        let builder = self.builder.name(name);
580        Self { builder, ..self }
581    }
582
583    /// Spawn the provided task with this builder's settings and store it in the
584    /// [`JoinSet`], returning an [`AbortHandle`] that can be used to remotely
585    /// cancel the task.
586    ///
587    /// # Returns
588    ///
589    /// An [`AbortHandle`] that can be used to remotely cancel the task.
590    ///
591    /// # Panics
592    ///
593    /// This method panics if called outside of a Tokio runtime.
594    ///
595    /// [`AbortHandle`]: crate::task::AbortHandle
596    #[track_caller]
597    pub fn spawn<F>(self, future: F) -> std::io::Result<AbortHandle>
598    where
599        F: Future<Output = T>,
600        F: Send + 'static,
601        T: Send,
602    {
603        Ok(self.joinset.insert(self.builder.spawn(future)?))
604    }
605
606    /// Spawn the provided task on the provided [runtime handle] with this
607    /// builder's settings, and store it in the [`JoinSet`].
608    ///
609    /// # Returns
610    ///
611    /// An [`AbortHandle`] that can be used to remotely cancel the task.
612    ///
613    ///
614    /// [`AbortHandle`]: crate::task::AbortHandle
615    /// [runtime handle]: crate::runtime::Handle
616    #[track_caller]
617    pub fn spawn_on<F>(self, future: F, handle: &Handle) -> std::io::Result<AbortHandle>
618    where
619        F: Future<Output = T>,
620        F: Send + 'static,
621        T: Send,
622    {
623        Ok(self.joinset.insert(self.builder.spawn_on(future, handle)?))
624    }
625
626    /// Spawn the provided task on the current [`LocalSet`] with this builder's
627    /// settings, and store it in the [`JoinSet`].
628    ///
629    /// # Returns
630    ///
631    /// An [`AbortHandle`] that can be used to remotely cancel the task.
632    ///
633    /// # Panics
634    ///
635    /// This method panics if it is called outside of a `LocalSet`.
636    ///
637    /// [`LocalSet`]: crate::task::LocalSet
638    /// [`AbortHandle`]: crate::task::AbortHandle
639    #[track_caller]
640    pub fn spawn_local<F>(self, future: F) -> std::io::Result<AbortHandle>
641    where
642        F: Future<Output = T>,
643        F: 'static,
644    {
645        Ok(self.joinset.insert(self.builder.spawn_local(future)?))
646    }
647
648    /// Spawn the provided task on the provided [`LocalSet`] with this builder's
649    /// settings, and store it in the [`JoinSet`].
650    ///
651    /// # Returns
652    ///
653    /// An [`AbortHandle`] that can be used to remotely cancel the task.
654    ///
655    /// [`LocalSet`]: crate::task::LocalSet
656    /// [`AbortHandle`]: crate::task::AbortHandle
657    #[track_caller]
658    pub fn spawn_local_on<F>(self, future: F, local_set: &LocalSet) -> std::io::Result<AbortHandle>
659    where
660        F: Future<Output = T>,
661        F: 'static,
662    {
663        Ok(self
664            .joinset
665            .insert(self.builder.spawn_local_on(future, local_set)?))
666    }
667}
668
669// Manual `Debug` impl so that `Builder` is `Debug` regardless of whether `T` is
670// `Debug`.
671#[cfg(all(tokio_unstable, feature = "tracing"))]
672#[cfg_attr(docsrs, doc(cfg(all(tokio_unstable, feature = "tracing"))))]
673impl<'a, T> fmt::Debug for Builder<'a, T> {
674    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
675        f.debug_struct("join_set::Builder")
676            .field("joinset", &self.joinset)
677            .field("builder", &self.builder)
678            .finish()
679    }
680}