In this post I would like to talk about the sometimes misunderstanding of the concept of task. I will also try to show a few non-obviousities when working with
TaskCompletionSource and just completed tasks, their solution and origins.
Problem
Suppose we have some code:static Task<TResult> ComputeAsync<TResult>(Func<TResult> highCpuFunc) { var tcs = new TaskCompletionSource<TResult>(); try { TResult result = highCpuFunc(); tcs.SetResult(result);
And an example of use: try { Task.WaitAll(ComputeAsync(() => { // do work })); } catch (AggregateException) { } Console.WriteLine("Everything is gonna be ok");
Is there a problem with the code above along with an example? If so, which ones? It seems to be an AggregateException catch.
Everything is gonna be ok ?
NB : The subject of cancellation of tasks will be revealed next time, so we will not consider the absence of a cancellation token.
Origins
The concept of tasks is very closely related to the idea of asynchrony, which is sometimes confused with multi-threaded execution. And this, in turn, leads to the conclusion that every challenge of a task is something performed somewhere there.
')
Task can be executed in the same thread as the calling code. Moreover, the execution of the task does not necessarily mean the execution of instructions - it can be just Task.FromResult, for example.
So, the problem number 1 lies in the example of use: it is necessary to catch InvalidOperationException (why it will become obvious just below) or any other exception along with AggregateException.
Task.WhenAll and co. methods are documented as
throws AggregateException, ArgumentNullException, ObjectDisposedException
is true.
But it is necessary to understand the sequence of code execution: if the ComputeAsync body starts to run in the output flow, then it will not reach Task.WhenAll. Although it is a little and not obvious.
The correct option is: try { Task.WaitAll(ComputeAsync(() => { // do work })); } catch (AggregateException) { } catch (InvalidOperationException) { } Console.WriteLine("Everything is gonna be ok");
OK, we figured it out. Go ahead.
By itself, the API provided by the
TaskCompletionSource class is highly intuitive. The methods
SetResult ,
SetCanceled ,
SetException speak for themselves. But here lies the problem: they manipulate the
state of the total task.
Hmm ... Already understood the focus? Consider more.
In the
ComputeAsync method
there is a section of code where SetResult is set, changing the status of the task to RanToCompletion.
After that, in the line with
evil code
(which seems to hint) if an exception is thrown, it will be processed and caught in a SetException, which will be an attempt # 2 to change the status of the task.
At the same time, the very state of the Task class is
immutable .
NB : Why is this behavior good? Consider an example:
static async Task<bool> ReadContentTwice() { using (var stringReader = new StringReader("blabla")) { Task<string> task = stringReader.ReadToEndAsync(); string content = await task;
If the task status could be changed, this would lead to a situation of non-deterministic code behavior. And we know the rule that mutable structs are “evil” (although Task is a class, but still a question of behavior is relevant).
Then everything is simple - InvalidOperationException and blah blah.
Decision
Everything is very obvious: to call SetResult right before leaving the method
always .
Ordered SetResult static Task<TResult> ComputeAsync<TResult>(Func<TResult> highCpuFunc) { var tcs = new TaskCompletionSource<TResult>(); try { TResult result = highCpuFunc(); // some evil code // set result as last action tcs.SetResult(result); } catch (Exception exc) { tcs.SetException(exc); } return tcs.Task; }
-
Why do not we consider methods TrySetResult , TrySetCanceled , TrySetException ?!
To use these, you must answer the question:
- Is the use of TaskCompletionSource itself limited only by this method?
If the answer to the question above is NO, then you should use TryXXX. This also includes the
patterns APM, EAP.
If the code is simple as in the original example, simple ordering of the methods.
Bonus track
To call Task.FromResult every time is inefficient. Why waste memory? To do this, you can use the built-in features of the framework ... which are not!
Exactly. The concept of CompletedTask came only in .NET
4.6 . And (as you already guessed) there is some
feature .
Let's start with a fresh one: the new property of the Task.CompletedTask property: is simply a static property of type Task (I want to note exactly what the
non-generic version is). Well, OK. It is hardly useful, because rarely there are no results.
And also ... the documentation says:
May not always return the same instance . Made specially.
That never kesherovat and not to compare to value (ie the link) on Task.CompletedTask when checking for completed task.
The solution to this problem is very simple:
public static class CompletedTaskSource<T> { private static readonly Task<T> CompletedTask = Task.FromResult(default(T)); public static Task<T> Task { get { return CompletedTask; } } }
And that's all. Fortunately, for .NET 4, there is a separate
Microsoft Async NuGet package, which allows you to compile C # 5 code for .NET 4 + brings the missing Task.FromResult, etc.