Skip to content

Consider adding TaskCompletionSource.SetFromTask(Task) #47998

Closed

Description

@davidfowl

EDITED by @stephentoub on 1/21/2023:

  • I added Try methods.
namespace System.Collections.Threading.Tasks
{
    public class TaskCompletionSource
    {
+        public void SetFromTask(Task task);
+        public bool TrySetFromTask(Task task);
    }

    public class TaskCompletionSource<T>
    {
+        public void SetFromTask(Task<T> task);
+        public bool TrySetFromTask(Task<T> task);
    }
}

These methods require that the task argument has already completed and transfer the result/exception/cancellation state over to the TCS's Task.

Background and Motivation

A common operation when writing async code is to execute code that returns a task and complete. This usually involves creating a TaskCompletionSource to represent the operation, then executing the operation that returns a task at point in the future, then setting the result on completion to either failed, cancelled or success.

Proposed API

namespace System.Collections.Threading.Tasks
{
    public class TaskCompletionSource
    {
+       public void SetFromTask(Task task);
    }

    public class TaskCompletionSource<T>
    {
+       public void SetFromTask(Task<T> task);
    }
}

Usage Examples

public class Connection
{
    private TaskCompletionSource _tcs = new();
    private CancealltionTokenSource _cts = new();

    public Task Run()
    {
        ThreadPool.QueueUserWorkItem(_ => Start());
        return _tcs.Task;
    }
    
    public void Start()
    {
        tcs.SetFromTask(NetworkHelper.StartConnectionAsync(_cts));
    }
    // ...
}

This is a common pattern when you need to kick off an async operation and can't directly wait the operation inline.

Alternative Designs

Do this manually.

public static class TaskCompletionSourceExtensions
{
    public void SetFromTask(this TaskCompletionSource  task, Task task)
    {
        async Task Continue()
        {
            try
            {
                await task;
                tcs.TrySetResult();
            }
            catch (OperationCancelledException ex)
            {
               tcs.TrySetCancelled(ex.Cancelled);
            }
            catch (Exception ex)
            {
               tcs.TrySetException(ex);
            }
        }
        _ = Continue();
    }
}

Risks

None.

Activity

  1. added
    api-suggestionEarly API idea and discussion, it is NOT ready for implementation
    on Feb 8, 2021
  2. ghost added
    untriagedNew issue has not been triaged by the area owner
    on Feb 8, 2021
  3. stephentoub commented on Feb 8, 2021

    @stephentoub
    Member

    We've considered such a thing in the past, e.g. tcs.SetFromTask(task). But we frequently found that there was a better way of writing it. For example, in your usage example:

    public class Connection
    {
        private TaskCompletionSource _tcs = new();
        private CancealltionTokenSource _cts = new();
    
        public Task Run()
        {
            ThreadPool.QueueUserWorkItem(_ => Start());
            return _tcs.Task;
        }
        
        public void Start()
        {
            NetworkHelper.StartConnectionAsync(_cts).ContinueWith(tcs);
        }
        // ...
    }

    passing the tcs into StartConnectionAsync and having it complete it.

    public class Connection
    {
        private TaskCompletionSource _tcs = new();
        private CancealltionTokenSource _cts = new();
    
        public Task Run()
        {
            ThreadPool.QueueUserWorkItem(_ => Start());
            return _tcs.Task;
        }
        
        public void Start()
        {
            NetworkHelper.StartConnectionAsync(_tcs, _cts);
        }
        // ...
    }

    or if StartConnectionAsync actually returned a Task (since I know how much you love async void methods), just using Task.Run:

    public class Connection
    {
        private CancealltionTokenSource _cts = new();
    
        public Task Run() => Task.Run(() => Start());
        
        public Task Start() => NetworkHelper.StartConnectionAsync(_cts);
        // ...
    }

    which does such transfer automatically.

  4. davidfowl commented on Feb 8, 2021

    @davidfowl
    MemberAuthor
    • I have a delegate that I can't change the signature of, so I can't pass the all the way down.
    • Task.Run doesn't work for me because I need to get rid of the ExecutionContext. I'm using my new favorite method UnsafeQueueUserWorkItem.
    • I prefer the name SetFromTask on the TaskCompletionSource.
  5. changed the title [-]Consider adding Task.ContinueWith(TaskCompletionSource)[/-] [+]Consider adding TaskCompletionSource.SetFromTask(Task)[/+] on Feb 8, 2021
  6. stephentoub commented on Feb 8, 2021

    @stephentoub
    Member

    Task.Run doesn't work for me because I need to get rid of the ExecutionContext

    You can of course use SuppressFlow. Yes, it's more code, but such a SetFromTask would need to be used enough to make it worthwhile.

    Are you using UnsafeQueueUserWorkItem purely for the ExecutionContext behavior? Or do you actually need to queue the invocation of the async operation for some reason?

    This still seems to me to be too niche to warrant exposing. The sole place we have such a SetFromTask method is in the implementation of Task.Run itself.

    How many places would you actually use this?

  7. davidfowl commented on Feb 8, 2021

    @davidfowl
    MemberAuthor

    Are you using UnsafeQueueUserWorkItem purely for the ExecutionContext behavior? Or do you actually need to queue the invocation of the async operation for some reason?

    I need both behaviors.

    This still seems to me to be too niche to warrant exposing. The sole place we have such a SetFromTask method is in the implementation of Task.Run itself.

    Yea, it's not a big deal. I wrote a helper but I remembered that I also wrote a helper when we did the original SignalR (https://github.com/SignalR/SignalR/blob/75126981cd165fb57db4e38e9379e651fec1ecdf/src/Microsoft.AspNet.SignalR.Core/TaskAsyncHelper.cs#L315)

    How many places would you actually use this?

    I'll look around. I don't think it's critical at the moment, I filed this because it came up again in code I was writing and I didn't the first time

  8. theodorzoulias commented on Feb 9, 2021

    @theodorzoulias
    Contributor

    @davidfowl an existing alternative of the proposed API is to use a cold nested task and the Unwrap method. The Unwrap resembles the TaskCompletionSource.SetFromTask, in that it returns a proxy of the inner task. The completion status of the inner task is propagated through the proxy.

    public class Connection
    {
        private Task<Task> _taskTask;
        private CancellationTokenSource _cts = new();
    
        public Connection()
        {
            _taskTask = new Task<Task>(() => NetworkHelper.StartConnectionAsync(_cts));
        }
    
        public Task Run()
        {
            ThreadPool.QueueUserWorkItem(_ => _taskTask.RunSynchronously(TaskScheduler.Default));
            return _taskTask.Unwrap();
        }
        // ...
    }
  9. davidfowl commented on Feb 9, 2021

    @davidfowl
    MemberAuthor

    Man I forgot about Unwrap m. Async/await made me forget past trauma.

  10. davidfowl commented on Feb 9, 2021

    @davidfowl
    MemberAuthor

    Do cold tasks capture the execution context?

  11. theodorzoulias commented on Feb 9, 2021

    @theodorzoulias
    Contributor

    Do cold tasks capture the execution context?

    I wish I knew that!

  12. stephentoub commented on Feb 9, 2021

    @stephentoub
    Member

    Yes. ExecutionContext is captured in Task's delegate-based ctor.

  13. 17 remaining items

  14. stephentoub commented on Apr 14, 2023

    @stephentoub
    Member

    I think it'd be much cleaner if {Try}SetFromTask was simply about transferring the state of an already-completed task over to a TCS. If you wanted that to happen when the task completes (and you weren't otherwise already registering a continuation with it for other reasons), you'd hook up a continuation explicitly, e.g. rather than:

    tcs.SetFromTask(task);

    that would implicitly add a continuation to task in this proposal, you'd instead do:

    task.ContinueWith(tcs.SetFromTask);

    or:

    await task.NoThrow();
    tcs.SetFromTask(task);

    Yes, they're arguably a bit more complicated to use, but I also think they're more easily explainable, they address the issue of what to do if the TCS is completed before a previously SetFromTask'd task completes, and it leaves the semantics of SetFromTask being effectively the same as the existing SetResult/Exception/Canceled. You get to be very explicit about what to do if you lose a race condition to complete the TCS, you get to manually control the continuation and can then do things like cancel it or do additional work as part of the continuation, etc.

  15. modified the milestones: 8.0.0, Future on Jul 24, 2023
  16. self-assigned this
    on Dec 26, 2023
  17. modified the milestones: Future, 9.0.0 on Dec 26, 2023
  18. added
    api-ready-for-reviewAPI is ready for review, it is NOT ready for implementation
    and removed
    api-needs-workAPI needs work before it is approved, it is NOT ready for implementation
    on Jan 3, 2024
  19. bartonjs commented on Jan 16, 2024

    @bartonjs
    Member

    Video

    • The task parameters were renamed to completedTask to help indicate that the parameter must always be in a task.IsCompleted == true state.
    namespace System.Collections.Threading.Tasks
    {
        public partial class TaskCompletionSource
        {
            public void SetFromTask(Task completedTask);
            public bool TrySetFromTask(Task completedTask);
        }
    
        public partial class TaskCompletionSource<T>
        {
            public void SetFromTask(Task<T> completedTask);
            public bool TrySetFromTask(Task<T> completedTask);
        }
    }
  20. added
    api-approvedAPI was approved in API review, it can be implemented
    and removed
    api-ready-for-reviewAPI is ready for review, it is NOT ready for implementation
    on Jan 16, 2024
  21. ghost added
    in-prThere is an active PR which will close this issue when it is merged
    on Jan 17, 2024
  22. ghost removed
    in-prThere is an active PR which will close this issue when it is merged
    on Jan 20, 2024
  23. locked and limited conversation to collaborators on Feb 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions