Repository navigation
Consider adding TaskCompletionSource.SetFromTask(Task) #47998
Copy link
Copy link
Closed
Labels
api-approvedAPI was approved in API review, it can be implementedAPI was approved in API review, it can be implementedarea-System.Threading.Tasks
Milestone
Description
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.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
api-approvedAPI was approved in API review, it can be implementedAPI was approved in API review, it can be implementedarea-System.Threading.Tasks
Activity
stephentoub commentedon Feb 8, 2021
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:
passing the tcs into StartConnectionAsync and having it complete it.
or if StartConnectionAsync actually returned a Task (since I know how much you love async void methods), just using Task.Run:
which does such transfer automatically.
davidfowl commentedon Feb 8, 2021
UnsafeQueueUserWorkItem.SetFromTaskon the TaskCompletionSource.[-]Consider adding Task.ContinueWith(TaskCompletionSource)[/-][+]Consider adding TaskCompletionSource.SetFromTask(Task)[/+]stephentoub commentedon Feb 8, 2021
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 commentedon Feb 8, 2021
I need both behaviors.
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)
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 commentedon Feb 9, 2021
@davidfowl an existing alternative of the proposed API is to use a cold nested task and the
Unwrapmethod. TheUnwrapresembles theTaskCompletionSource.SetFromTask, in that it returns a proxy of the inner task. The completion status of the inner task is propagated through the proxy.davidfowl commentedon Feb 9, 2021
Man I forgot about Unwrap m. Async/await made me forget past trauma.
davidfowl commentedon Feb 9, 2021
Do cold tasks capture the execution context?
theodorzoulias commentedon Feb 9, 2021
I wish I knew that!
stephentoub commentedon Feb 9, 2021
Yes. ExecutionContext is captured in Task's delegate-based ctor.
17 remaining items
stephentoub commentedon 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:
that would implicitly add a continuation to
taskin this proposal, you'd instead do:or:
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.
bartonjs commentedon Jan 16, 2024
Video
taskparameters were renamed tocompletedTaskto help indicate that the parameter must always be in atask.IsCompleted == truestate.