When reviewing work by a contractor, I came upon the following extension method:
public static class TaskExtensions
{
public static async Task<IEnumerable<TResult>> WhenAllSynchronousExecution<TResult>(this IEnumerable<Task<TResult>> tasks, CancellationToken token = default(CancellationToken))
{
var results = new List<TResult>();
var exceptions = new List<Exception>();
foreach (var task in tasks)
{
if (task == null)
{
throw new ArgumentNullException(nameof(tasks));
}
if (token.IsCancellationRequested) {
exceptions.Add(new OperationCanceledException("Tasks collection cancelled", token));
break;
}
try
{
results.Add(await task);
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
if (exceptions.Any()) {
throw new AggregateException(exceptions);
}
return results;
}
}
To this looks to be exactly what Task.WhenAll<TResult> already does. quote the remarks:
The call to WhenAll(IEnumerable<Task>) method does not block the calling thread. However, a call to the returned Result property does block the calling thread.
If any of the supplied tasks completes in a faulted state, the returned task will also complete in a Faulted state, where its exceptions will contain the aggregation of the set of unwrapped exceptions from each of the supplied tasks.
If none of the supplied tasks faulted but at least one of them was canceled, the returned task will end in the Canceled state.
If none of the tasks faulted and none of the tasks were canceled, the resulting task will end in the RanToCompletion state. The Task.Result property of the returned task will be set to an array containing all of the results of the supplied tasks in the same order as they were provided (e.g. if the input tasks array contained t1, t2, t3, the output task's Task.Result property will return an TResult[] where arr[0] == t1.Result, arr[1] == t2.Result, and arr[2] == t3.Result).
If the tasks argument contains no tasks, the returned task will immediately transition to a RanToCompletion state before it's returned to the caller. The returned TResult[] will be an array of 0 elements.
So to me it seems they've recreated an existing method. The only difference seems to be the checking of the CancellationToken, but I don't see mych added value there (it's not used in the code). And maybe that it's an extension method, but that's also not used in the code (it's called like var task = TaskExtensions.WhenAllSynchronousExecution(tasks);)
But this colleague is quite "sensitive". So I need to pick my battles. So is this just a custom,meagre recreation of Task.WhenAll<TResult>?
Nope, there are quite a lot of differences between the Task.WhenAll and the WhenAllSynchronousExecution. Here are some of them:
The Task.WhenAll materializes immediately the IEnumerable<Task<TResult>> sequence, by enumerating it and storing the tasks in an array. This ensures that all the tasks are up and running, before attaching continuations on them. The WhenAllSynchronousExecution doesn't do that, so it could potentially start each task after the completion of the previous task (sequentially), depending on how the enumerable sequence is implemented.
The Task.WhenAll doesn't take a CancellationToken, so when it completes you can be 100% sure that all the tasks have completed. It is always possible to fire-and-forget a Task.WhenAll task, just like any other Task, with the WaitAsync method (.NET 6), so there is no reason to bake this functionality in every async method.
The Task.WhenAll propagates the exceptions directly, as the InnerExceptions of the Task.Exception property. The WhenAllSynchronousExecution wraps the exceptions in an additional AggregateException. So you can't switch between the two APIs without rethinking your exception-handling code.
The Task.WhenAll completes as Canceled when none of the tasks is Faulted, and at least one is Canceled. The WhenAllSynchronousExecution propagates the cancellation as error, always.
The WhenAllSynchronousExecution captures the SynchronizationContext in its internal await points, so it might cause a deadlock if the caller blocks on async code.
The Task.WhenAll checks for null task instances synchronously. The WhenAllSynchronousExecution does the check during the loop that awaits each task.
Correction: Actually the integrated CancellationToken is a useful feature, that has been requested on GitHub.
I think the key to figuring out if this method has any reason to exist is to confirm if the tasks are started before the method is called.
If the tasks are not started then the method should probably be called ~WhenAllSequential.
If the tasks are started then this method seems not to add anything good.
In any case, I would argue against having this method as an extension method:
Having this method shown in auto-completion hints will sooner or later make someone pass already started tasks and think the tasks will be executed sequentially but it's the caller of this method who is in charge when the tasks are started.
Even the official WhenAll is not an extension method.
Related
The code discussed here is written in C# and executed with .netcore 3.1
I have the following piece of code, which starts a workload in the background without awaiting for it to complete (fire and forget):
public void StartBackgroundWork(IAsyncDisposable resource, CancellationToken token)
{
// some background work is started in a fire and forget manner
_ = Task.Run(async () =>
{
try
{
// here I perform my background work. Regardless of the outcome resource must be released as soon as possible
// I want that cancellation requests coming from the provided cancellation token are correctly listened by this code
// So, I pass the cancellation token everywhere
await Task.Delay(1500, token);
}
finally
{
// here I need to release the resource. Releasing this resource is important and must be done as soon as possible
await resource.DisposeAsync();
}
}, token);
}
There are three important points:
the background work is started in a fire and forget manner. I'm not interested in awaiting its completion
the provided cancellation token is important and the background work must listed to incoming cancellation requests
the provided resource (IAsyncDisposable) must be released as soon as possible, regardless of the outcome of the background work. In order to release the resource a call to DisposeAsync is required.
The problem with this code is that the cancellation token is passed to Task.Run invokation. If the token is canceled before the execution of the async delegate starts, the async delegate is never executed and so the finally block is never executed. By doing so the requirement of releasing the IAsyncDisposable resource is not met (basically, DisposeAsync is never called).
The simplest way to solve this issue is not providing the cancellation token when Task.Run is invoked. That way, the async delegate is always executed and so the finally block is executed too. The code inside the async delegate listens to cancellation requests, so the requirement of cancel the execution is met too:
public void StartBackgroundWork(IAsyncDisposable resource, CancellationToken token)
{
// some background work is started in a fire and forget manner
_ = Task.Run(async () =>
{
try
{
// here I perform my background work. Regardless of the outcome resource must be released as soon as possible
// I want that cancellation requests coming from the provided cancellation token are correctly listened by this code
// So, I pass the cancellation token everywhere
await Task.Delay(1500, token);
}
finally
{
// here I need to release the resource. Releasing this resource is important and must be done as soon as possible
await resource.DisposeAsync();
}
}, CancellationToken.None);
}
I'm asking myself whether the release of the IAsyncDisposable resource should, instead, be delegated to a continuation task. The code refactored by using this approach is the following:
public void StartBackgroundWork(IAsyncDisposable resource, CancellationToken token)
{
// some background work is started in a fire and forget manner
_ = Task.Run(async () =>
{
// here I perform my background work. Regardless of the outcome resource must be released as soon as possible
// I want that cancellation requests coming from the provided cancellation token are correctly listened by this code
// So, I pass the cancellation token everywhere
await Task.Delay(1500, token);
},
token).ContinueWith(async _ =>
{
// release the IAsyncDisposable resource here, afte the completion of the antecedent task and regardless
// of the antecedent task actual state
await resource.DisposeAsync();
});
}
I'm not really familiar with ContinueWith gotchas, so my questions are the following:
do I have the guarantee that the continuation is always executed, even if the cancellation token is canceled before the execution of the antecedent task starts ?
is there any issue in providing an async delegate to the invokation of ContinueWith ? Is the execution of the async delegate fully completed as expected ?
What is the best approach ? Passing CancellationToken.None to the invokation of Task.Run, or relying on the continuation by using ContinueWith ?
IMPORTANT NOTE: I know that using Task.Run is not the best approach in a server application (more on that can be found here), so there are probably much better ways of designing my overall architecture. I posted this question to better understanding the actual behavior of ContinueWith, because I'm not really familiar with its usage (in modern .NET code it is largely replaced by the usage of async await).
You could consider using the await using statement, that handles the asynchronous disposal of the resource automatically:
public async void StartBackgroundWork(IAsyncDisposable resource, CancellationToken token)
{
await using var _ = resource;
try
{
await Task.Run(async () =>
{
await Task.Delay(1500, token);
}, token);
} catch (OperationCanceledException) { }
}
I also converted your fire-and-forget task to an async void (aka fire-and-crash) method. In case the unthinkable happens and your code has a bug, instead of the app continue running with an unobserved exception having occurred, resulting possibly to corrupted application state, the whole app will crash, forcing you to fix the bug ASAP.
But honestly creating a disposable resource in one method and disposing it in another is a smelly design. Ideally the method that created the resource should be responsible for disposing it finally.
I think Theodor has a great answer; I'm just going to answer some of your other questions:
do I have the guarantee that the continuation is always executed, even if the cancellation token is canceled before the execution of the antecedent task starts ?
ContinueWith will execute its delegate even of the antecedent task is already completed. In this specific case, there is no "guarantee" simply because of the nature of fire-and-forget.
is there any issue in providing an async delegate to the invokation of ContinueWith ?
ContinueWith is not async-aware, so the return type of ContinueWith is surprising for most developers. Since your code discards the return type, that's not a concern here.
Is the execution of the async delegate fully completed as expected ?
In this case, most likely, but it really depends on what "expected" means. Like all other fire-and-forget code, you can't guarantee completion. ContinueWith has an additional wrinkle: it executes its delegate using a TaskScheduler, and the default TaskScheduler is not TaskScheduler.Default but is actually TaskScheduler.Current. So I always recommend passing an explicit TaskScheduler for clarity if you really need to use ContinueWith.
What is the best approach ? Passing CancellationToken.None to the invokation of Task.Run, or relying on the continuation by using ContinueWith ?
Just drop the second argument to Task.Run.
I'll go further than that: Task.Run probably shouldn't even take a CancellationToken. I have yet to see a scenario where it's useful. I suspect the CancellationToken part of the API was copied from TaskFactory.StartNew (where it is rarely useful), but since Task.Run always uses TaskScheduler.Default, providing a CancellationToken is not useful in practice.
P.S. I recently wrote a short series on the proper solution for fire-and-forget on ASP.NET.
I'm not after a solution here, more an explanation of what's going on. I've refactored this code to prevent this problem but I'm intrigued why this call deadlocked. Basically I have a list of head objects and I need to load each ones details from a DB repository object (using Dapper). I attempted to do this using ContinueWith but it failed:
List<headObj> heads = await _repo.GetHeadObjects();
var detailTasks = heads.Select(s => _changeLogRepo.GetDetails(s.Id)
.ContinueWith(c => new ChangeLogViewModel() {
Head = s,
Details = c.Result
}, TaskContinuationOptions.OnlyOnRanToCompletion));
await Task.WhenAll(detailTasks);
//deadlock here
return detailTasks.Select(s => s.Result);
Can someone explain what caused this deadlock? I tried to get my head round what has happened here but I'm not sure. I'm presuming it's something to do with calling .Result in the ContinueWith
Additional information
This is a webapi app called in an async context
The repo calls are all along the lines of:
public async Task<IEnumerable<ItemChangeLog>> GetDetails(int headId)
{
using(SqlConnection connection = new SqlConnection(_connectionString))
{
return await connection.QueryAsync<ItemChangeLog>(#"SELECT [Id]
,[Description]
,[HeadId]
FROM [dbo].[ItemChangeLog]
WHERE HeadId = #headId", new { headId });
}
}
I have since fixed this issue with the following code:
List<headObj> heads = await _repo.GetHeadObjects();
Dictionary<int, Task<IEnumerable<ItemChangeLog>>> tasks = new Dictionary<int, Task<IEnumerable<ItemChangeLog>>>();
//get details for each head and build the vm
foreach(ItemChangeHead head in heads)
{
tasks.Add(head.Id, _changeLogRepo.GetDetails(head.Id));
}
await Task.WhenAll(tasks.Values);
return heads.Select(s => new ChangeLogViewModel() {
Head = s,
Details = tasks[s.Id].Result
});
The issue is actually a combination of the above. An enumeration of tasks was created where each time the enumeration is iterated, a fresh GetDetails call. A ToList call on this Select would fix the deadlock. Without solidifying the results of the enumerable (putting them in a list), the WhenAll call evaluates the enumerable and waits for the resulting tasks asynchronously without issue, but when the returned Select statement evaluates, it's iterating and synchronously waiting on the results of tasks resulting from fresh GetDetails and ContinueWith calls that have not yet completed. All of this synchronous waiting is likely occurring while trying to serialize the response.
As to why that synchronous wait causes a deadlock, the mystery is in how await does things. It completely depends on what you're calling. An await is actually just retrieval of an awaiter via any scope-visible qualifying GetAwaiter method and registration of a callback that immediately calls GetResult on the awaiter when the work is complete. A qualifying GetAwaiter method can be an instance or extension method that returns an object having an IsCompleted property, a parameterless GetResult method (any return type, including void - result of await), and either INotifyCompletion or ICriticalNotifyCompletion interfaces. The interfaces both have OnComplete methods to register the callback. There's a mind-boggling chain of ContinueWith and await calls going on here and much of it depends on the runtime environment. The default behavior of the await you get from a Task<T> is to use SynchronizationContext.Current (I think via TaskScheduler.Current) to invoke the callback or, if that's null to use the thread pool (I think via TaskScheduler.Default) to invoke the callback. A method containing an await gets wrapped as a Task by some CompilerServices class (forgot the name), giving callers of the method the above described behaviour wrapping whatever implementation you are awaiting.
A SynchronizationContext can also customize this, but typically each context invokes on it's own single thread. If such an implementation is present on SynchronizationContext.Current when await is called on a Task, and you synchronously wait for the Result (which itself is contingent on an invoke to the waiting thread), you get a deadlock.
On the other hand, if you broke your as-is method out to another thread, or call ConfigureAwait on any of the tasks, or hide the current scheduler for your ContinueWith calls, or set your own SynchronizationContext.Current (not recommended), you change all the above.
Say I have a Task that generates an int, and a callback that accepts an int:
Task<int> task = ...;
Action<int> f = ...;
Now I want to set it up so that once the task has completed and returned its integer result, the callfack f will be called with that integer – without requiring the main thread to wait for the task to complete:
As I understand it, the typical solution for this is the Task.ContinueWith method:
task.ContinueWith(t => f(t.Result));
However, one could also get a TaskAwaiter for the task, and use its event-like interface:
TaskAwaiter<int> awaiter = task.GetAwaiter();
awaiter.OnCompleted(() => f(awaiter.GetResult()));
Now, I realize that TaskAwaiter is not intended for common use in application code, and mainly exists to be used internally by the await keyword.
But in order to deepen my understanding of the TPL, I'm wondering:
Is there any practical difference between solutions (1) and (2)?
For example,
...regarding on which thread the callback will be invoked?
...regarding what happens when an exception is thrown in the task or callback?
...any other side-effects?
One difference is that
task.ContinueWith(t => f(t.Result));
will not capture current synchronization context, and in, for example, UI applications - callback will be executed on thread pool thread. While
TaskAwaiter<int> awaiter = task.GetAwaiter();
awaiter.OnCompleted(() => f(awaiter.GetResult()));
will capture synchronization context, and callback will be executed on UI thread.
Of course you can do the same with ContinueWith:
task.ContinueWith(r => f(r.Result), TaskScheduler.FromCurrentSynchronizationContext());
But that's not what you used in question. So ways provided in your question are different at least in that regard.
There is also a difference in exception representation, accessing Task.Result if task has faulted will throw AggregateException (with one or more exceptions as inner exceptions), while accessing awaiter.GetResult() will not wrap thrown exception in AggregateException and will rethrow it as is (and if there were multiple exceptions, like from Task.WhenAll - all except one will be ignored and just one will be thrown).
So here is what I'm trying to achieve. I launch a task and don't do wait/result on it. To ensure that if launched task goes to faulted state (for e.g. say throw an exception) I crash the process by calling Environment FailFast in Continuation.
How the problem I'm facing is that If I ran below code, Inside ContinueWith, the status of the task (which threw exception) shows up as "RanToCompletion". I expected it to be Faulted State.
private Task KickOfTaskWorkAsync()
{
var createdTask = Task.Run(() => this.RunTestTaskAsync(CancellationToken.None).ConfigureAwait(false), CancellationToken.None);
createdTask.ContinueWith(
task => Console.WriteLine("Task State In Continue with => {0}", task.Status));
return createdTask;
}
private async Task RunTestTaskAsync(CancellationToken cancellationToken)
{
throw new Exception("CrashingRoutine: Crashing by Design");
}
This is really strange :( If I remove the 'ConfigureAwait(false)' inside Task.Run function call, the task does goes to Faulted state inside Continue with. Really at loss to explain what's going on and would appreciate some help from community.
[Update]: My colleague pointed out an obvious error. I am using ConfigureAwait while I make a call to RunTestAsync inside Test.Run even though I don't await it. In this case, ConfigureAwait doesn't return a Task to Task.Run. If I don't call ConfigureAwait, a Task does get returned and things work as expected.
Your error is a specific example of a broader category of mistake: you are not observing the Task you actually care about.
In your code example, the RunTestTaskAsync() returns a Task object. It completes synchronously (because there's no await), so the Task object it returns is already faulted when the method returns, due to the exception. Your code then calls ConfigureAwait() on this faulted Task object.
But all of this happens inside another Task, i.e. the one that you start when you call Task.Run(). This Task doesn't do anything to observe the exception, so it completes normally.
The reason you observe the exception when you remove the ConfigureAwait() call has nothing to do with the call itself. If you left the call and passed true instead, you would still fail to observe the exception. The reason you can observe the exception when you remove the call is that, without the call to ConfigureAwait(), the return value of the lambda expression is a Task, and this calls a different overload of Task.Run().
This overload is a bit different from the others. From the documentation:
Queues the specified work to run on the thread pool and returns a proxy for the task returned by function.
That is, while it still starts a new Task, the Task object it returns represents not that Task, but the one returned by your lambda expression. And that proxy takes on the same state as the Task it wraps, so you see it in the Faulted state.
Based on the code you posted, I would say that you shouldn't be calling Task.Run() in the first place. The following will work just as well, without the overhead and complication of the proxy:
static void Main(string[] args)
{
Task createdTask = RunTestTaskAsync();
createdTask.ConfigureAwait(false);
createdTask.ContinueWith(
task => Console.WriteLine("Task State In Continue with => {0}", task.Status)).Wait();
}
private static async Task RunTestTaskAsync()
{
throw new Exception("CrashingRoutine: Crashing by Design");
}
(I removed the CancellationToken values, because they have nothing at all to do with your question and are completely superfluous here.)
I use a set of tasks at times, and in order to make sure they are all awaited I use this approach:
public async Task ReleaseAsync(params Task[] TaskArray)
{
var tasks = new HashSet<Task>(TaskArray);
while (tasks.Any()) tasks.Remove(await Task.WhenAny(tasks));
}
and then call it like this:
await ReleaseAsync(task1, task2, task3);
//or
await ReleaseAsync(tasks.ToArray());
However, recently I have been noticing some strange behavior and set to see if there was a problem with the ReleaseAsync method. I managed to narrow it down to this simple demo, it runs in linqpad if you include System.Threading.Tasks. It will also work slightly modified in a console app or in an asp.net mvc controller.
async void Main()
{
Task[] TaskArray = new Task[]{run()};
var tasks = new HashSet<Task>(TaskArray);
while (tasks.Any<Task>()) tasks.Remove(await Task.WhenAny(tasks));
}
public async Task<int> run()
{
return await Task.Run(() => {
Console.WriteLine("started");
throw new Exception("broke");
Console.WriteLine("complete");
return 5;
});
}
What I don't understand is why the Exception never shows up anywhere. I would have figured that if the Tasks with the exception were awaited, it would throw. I was able to confirm this by replacing the while loop with a simple for each like this:
foreach( var task in TaskArray )
{
await task;//this will throw the exception properly
}
My question is, why doesn't the shown example throw the exception properly (it never shows up anywhere).
TL;DR: run() throws the exception, but you're awaiting WhenAny(), which doesn't throw an exception itself.
The MSDN documentation for WhenAny states:
The returned task will complete when any of the supplied tasks has completed. The returned task will always end in the RanToCompletion state with its Result set to the first task to complete. This is true even if the first task to complete ended in the Canceled or Faulted state.
Essentially what is happening is that the task returned by WhenAny simply swallows the faulted task. It only cares about the fact that the task is finished, not that it has successfully completed. When you await the task, it simply completes without error, because it is the internal task that has faulted, and not the one you're awaiting.
A Task not being awaited or not using its Wait() or Result() method, will swallow the exception by default. This behavior can be modified back to the way it was done in .NET 4.0 by crashing the running process once the Task was GC'd. You can set it in your app.config as follows:
<configuration>
<runtime>
<ThrowUnobservedTaskExceptions enabled="true"/>
</runtime>
</configuration>
A quote from this blog post by the Parallel Programming team in Microsoft:
Those of you familiar with Tasks in .NET 4 will know that the TPL has the notion of “unobserved” exceptions. This is a compromise between two competing design goals in TPL: to support marshaling unhandled exceptions from the asynchronous operation to the code that consumes its completion/output, and to follow standard .NET exception escalation policies for exceptions not handled by the application’s code. Ever since .NET 2.0, exceptions that go unhandled on newly created threads, in ThreadPool work items, and the like all result in the default exception escalation behavior, which is for the process to crash. This is typically desirable, as exceptions indicate something has gone wrong, and crashing helps developers to immediately identify that the application has entered an unreliable state. Ideally, tasks would follow this same behavior. However, tasks are used to represent asynchronous operations with which code later joins, and if those asynchronous operations incur exceptions, those exceptions should be marshaled over to where the joining code is running and consuming the results of the asynchronous operation. That inherently means that TPL needs to backstop these exceptions and hold on to them until such time that they can be thrown again when the consuming code accesses the task. Since that prevents the default escalation policy, .NET 4 applied the notion of “unobserved” exceptions to complement the notion of “unhandled” exceptions. An “unobserved” exception is one that’s stored into the task but then never looked at in any way by the consuming code. There are many ways of observing the exception, including Wait()’ing on the Task, accessing a Task’s Result, looking at the Task’s Exception property, and so on. If code never observes a Task’s exception, then when the Task goes away, the TaskScheduler.UnobservedTaskException gets raised, giving the application one more opportunity to “observe” the exception. And if the exception still remains unobserved, the exception escalation policy is then enabled by the exception going unhandled on the finalizer thread.
From the comment:
these [tasks] were tied to managed resources and I wanted to release them when
they became available instead of waiting for all of them to complete
and then releasing.
Using a helper async void method may give you the desired behavior for both removing the finished tasks from the list and immediately throwing unobserved exceptions:
public static class TaskExt
{
public static async void Observe<TResult>(Task<TResult> task)
{
await task;
}
public static async Task<TResult> WithObservation(Task<TResult> task)
{
try
{
return await task;
}
catch (Exception ex)
{
// Handle ex
// ...
// Or, observe and re-throw
task.Observe(); // do this if you want to throw immediately
throw;
}
}
}
Then your code might look like this (untested):
async void Main()
{
Task[] TaskArray = new Task[] { run().WithObservation() };
var tasks = new HashSet<Task>(TaskArray);
while (tasks.Any<Task>()) tasks.Remove(await Task.WhenAny(tasks));
}
.Observe() will re-throw the task's exception immediately "out-of-band", using SynchronizationContext.Post if the calling thread has a synchronization context, or using ThreadPool.QueueUserWorkItem otherwise. You can handle such "out-of-band" exceptions with AppDomain.CurrentDomain.UnhandledException).
I described this in more details here:
TAP global exception handler