Skip to content

Conversation

tyilo
Copy link
Contributor

@tyilo tyilo commented Aug 13, 2025

Motivation

I want to use the id from a JoinSet as a key in a BTreeMap.

Solution

Implement PartialOrd and Ord for Id.

@github-actions github-actions bot added R-loom-current-thread Run loom current-thread tests on this PR R-loom-multi-thread Run loom multi-thread tests on this PR labels Aug 13, 2025
@Darksonn Darksonn added A-tokio Area: The main tokio crate M-task Module: tokio/task labels Aug 13, 2025
Copy link
Member

@ADD-SP ADD-SP left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate your original requirements? Why do you want to save task id in an ordered map? Without this ordered map, what difficulties are hard to solve?

Currently, we only guarantee the uniqueness of task ids, their order has no special meaning.

@ADD-SP ADD-SP added the S-waiting-on-author Status: awaiting some action (such as code changes) from the PR or issue author. label Aug 13, 2025
@tyilo
Copy link
Contributor Author

tyilo commented Aug 13, 2025

I don't really care about the ordered property of a BTreeMap, but I think it would be faster than a HashMap.

For instance both std::io::ErrorKind and std::any::TypeId implements Ord even though they have no "natural" ordering.

See also C-COMMON-TRAITS


The concrete code I wanted it for is this (with HashMap replaced by BTreeMap):

use std::collections::HashMap;
use std::hash::Hash;

use tokio::task::JoinSet;

trait InsertOnce<K>
where
    K: ?Sized,
{
    type V: ?Sized;

    fn insert_once(&mut self, key: K, value: Self::V);
}

impl<K, V> InsertOnce<K> for HashMap<K, V>
where
    K: Eq + Hash,
{
    type V = V;

    fn insert_once(&mut self, key: K, value: Self::V) {
        let previous = self.insert(key, value);
        assert!(previous.is_none());
    }
}

impl<V> InsertOnce<usize> for Vec<Option<V>> {
    type V = V;

    fn insert_once(&mut self, key: usize, value: Self::V) {
        let entry = &mut self[key];
        assert!(entry.is_none());
        *entry = Some(value);
    }
}

pub async fn spawn_and_join_all<I, F, T>(futures: I, max_concurrent: usize) -> Vec<T>
where
    I: IntoIterator<Item = F>,
    F: Future<Output = T> + Send + 'static,
    T: Send + 'static,
{
    let mut futures = futures.into_iter().fuse();

    let mut set = JoinSet::new();
    let mut ids_to_index = HashMap::new();

    for f in futures.by_ref().take(max_concurrent) {
        let id = set.spawn(f).id();
        ids_to_index.insert_once(id, ids_to_index.len());
    }

    let mut total_started = set.len();
    let lower_bound = total_started + futures.size_hint().0;
    let mut result: Vec<Option<T>> = Vec::with_capacity(lower_bound);
    result.resize_with(total_started, Default::default);

    while let Some(r) = set.join_next_with_id().await {
        let (id, v) = r.unwrap();
        let index = ids_to_index.remove(&id).unwrap();
        result.insert_once(index, v);

        if let Some(f) = futures.next() {
            let id = set.spawn(f).id();
            ids_to_index.insert_once(id, total_started);
            total_started += 1;
            result.push(None);
        }
    }

    result.into_iter().map(|v| v.unwrap()).collect()
}

@ADD-SP
Copy link
Member

ADD-SP commented Aug 13, 2025

I believe that now we have two topics.

Task ID may be reused

    fn insert_once(&mut self, key: K, value: Self::V) {
        let previous = self.insert(key, value);
        assert!(previous.is_none());
    }

// ----- omitted -----

        if let Some(f) = futures.next() {
            let id = set.spawn(f).id();
            ids_to_index.insert_once(id, total_started);
            total_started += 1;
            result.push(None);
        }

There is a potential panic case.

Task IDs are unique relative to other currently running tasks. When a task completes, the same ID may be used for another task.

tokio::task::Id

Add PartialOrd and Ord for convenience

Thanks for your mentioning std::io::ErrorKind and std::any::TypeId, I searched their PRs and read the discussion.

I now think it makes sense to add these two features to make things easier (even they don't have natural ordering) for downstream users so I am not opposed to this change at this point.

However, firstly, it would be better taking care of the potential panic I mentioned above, then we can re-evaluate do we actually need a PartialOrd in your case.

Again, I am not opposed to this change after reading the above two PRs, but I still want to keep it as it is until there is a use case that absolutely requires this change.

@tyilo
Copy link
Contributor Author

tyilo commented Aug 13, 2025

There is a potential panic case.

Not really, because of the call to ids_to_index.remove(&id).

@ADD-SP ADD-SP removed the S-waiting-on-author Status: awaiting some action (such as code changes) from the PR or issue author. label Aug 13, 2025
Copy link
Member

@ADD-SP ADD-SP left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, I also learned some interesting stories today.

Since merging this PR will make PartialOrd and Ord be the part of semantic version guarantee, I would like to keep this PR open for a few days so that the community and other maintainers can participate in our discussion during this time window.

@carllerche
Copy link
Member

Given the TypeId precedence, I am fine adding Ord / PartialOrd implementation. I think it is fairly safe.

Copy link
Contributor

@Darksonn Darksonn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks.

@Darksonn Darksonn merged commit dd74c7c into tokio-rs:master Aug 15, 2025
101 of 104 checks passed
@tyilo tyilo deleted the task-id-ord branch August 15, 2025 09:44
@Darksonn Darksonn mentioned this pull request Oct 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-tokio Area: The main tokio crate M-task Module: tokio/task R-loom-current-thread Run loom current-thread tests on this PR R-loom-multi-thread Run loom multi-thread tests on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants