Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider adding TaskCompletionSource.SetFromTask(Task) #47998

Closed
davidfowl opened this issue Feb 8, 2021 · 18 comments · Fixed by #97077
Closed

Consider adding TaskCompletionSource.SetFromTask(Task) #47998

davidfowl opened this issue Feb 8, 2021 · 18 comments · Fixed by #97077
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.Threading.Tasks
Milestone

Comments

@davidfowl
Copy link
Member

davidfowl commented Feb 8, 2021

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.

@davidfowl davidfowl added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Feb 8, 2021
@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged New issue has not been triaged by the area owner label Feb 8, 2021
@dotnet-issue-labeler
Copy link

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

@ghost
Copy link

ghost commented Feb 8, 2021

Tagging subscribers to this area: @tarekgh
See info in area-owners.md if you want to be subscribed.

Issue Details

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 Task
    {
+       public void ContinueWith(TaskCompletionSource tcs);
    }

    public class Task<T>
    {
+       public void ContinueWith(TaskCompletionSource<T> tcs);
    }
}

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()
    {
        NetworkHelper.StartConnectionAsync(_cts).ContinueWith(tcs);
    }
    // ...
}

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 TaskExtensions
{
    public void ContinueWith(this Task task, TaskCompletionSource tcs)
    {
        async Task Continue()
        {
            try
            {
                await task;
                tcs.TrySetResult();
            }
            catch (OperationCancelledException ex)
            {
               tcs.TrySetCancelled(ex.Cancelled);
            }
            catch (Exception ex)
            {
               tcs.TrySetException(ex);
            }
        }
        _ = Continue();
    }
}

Risks

None.

Author: davidfowl
Assignees: -
Labels:

api-suggestion, area-System.Threading.Tasks, untriaged

Milestone: -

@stephentoub
Copy link
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.

@davidfowl
Copy link
Member Author

davidfowl commented Feb 8, 2021

  • 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.

@davidfowl davidfowl changed the title Consider adding Task.ContinueWith(TaskCompletionSource) Consider adding TaskCompletionSource.SetFromTask(Task) Feb 8, 2021
@stephentoub
Copy link
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?

@davidfowl
Copy link
Member Author

davidfowl commented Feb 8, 2021

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

@theodorzoulias
Copy link
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();
    }
    // ...
}

@davidfowl
Copy link
Member Author

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

@davidfowl
Copy link
Member Author

Do cold tasks capture the execution context?

@theodorzoulias
Copy link
Contributor

Do cold tasks capture the execution context?

I wish I knew that!

@stephentoub
Copy link
Member

stephentoub commented Feb 9, 2021

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

@carlossanlop
Copy link
Member

@davidfowl based on the answers, do you still believe we should continue pursuing this idea, or can we go ahead and close the issue?

@carlossanlop carlossanlop added needs author feedback and removed untriaged New issue has not been triaged by the area owner labels Feb 11, 2021
@carlossanlop carlossanlop added this to the Future milestone Feb 11, 2021
@davidfowl
Copy link
Member Author

Future will do for now

@ghost ghost added needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration and removed needs author feedback labels Feb 11, 2021
@SergeyTeplyakov
Copy link

I was working recently on 3 different projects at Microsoft - BuildXL, a distributed cache and AnyBuild. And I needed SetFromTask in all of them.

Of course, this is an example of selection bias, but I really think it would be helpful to have the API @davidfowl proposed.

@stephentoub stephentoub added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration labels Jan 21, 2023
@stephentoub stephentoub removed this from the Future milestone Apr 1, 2023
@stephentoub stephentoub added this to the 8.0.0 milestone Apr 1, 2023
@stephentoub
Copy link
Member

I added Try methods

I added these when I forgot that this proposal was building in the continuation into the operation. They make zero sense in that case. I also think that the SetFromTask name doesn't convey what it's actually being done here... my intuition is that it's just transferring the completion state of an existing task, just as eg SetResult is storing some existing result, in which case Try would make sense. And I don't think we can add the Try that means one thing and the non-Try that means something else. It's also not clear to me what the behavior of these methods would be if someone calls SetFromTask, then separately completes the TCS, and then the task passed to SetFromTask completes.

@stephentoub stephentoub added api-needs-work API needs work before it is approved, it is NOT ready for implementation and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Apr 13, 2023
@davidfowl
Copy link
Member Author

It's also not clear to me what the behavior of these methods would be if someone calls SetFromTask, then separately completes the TCS, and then the task passed to SetFromTask completes.

This is a good question. I think the normal behavior applies. The first one to call Try* wins. Maybe that means we need to change the name and also handle what happens if you call SetFromTask multiple times on different tasks ?

@stephentoub
Copy link
Member

stephentoub commented Apr 14, 2023

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.

@tannergooding tannergooding modified the milestones: 8.0.0, Future Jul 24, 2023
@stephentoub stephentoub self-assigned this Dec 26, 2023
@stephentoub stephentoub modified the milestones: Future, 9.0.0 Dec 26, 2023
@stephentoub stephentoub added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-needs-work API needs work before it is approved, it is NOT ready for implementation labels Jan 3, 2024
@bartonjs
Copy link
Member

bartonjs commented Jan 16, 2024

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);
    }
}

@bartonjs bartonjs added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Jan 16, 2024
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Jan 17, 2024
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Jan 20, 2024
@github-actions github-actions bot locked and limited conversation to collaborators Feb 20, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Threading.Tasks
Projects
No open projects
Development

Successfully merging a pull request may close this issue.

7 participants