📜 ⬆️ ⬇️

Rules for working with Tasks API. Part 2

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); // some evil code } catch (Exception exc) { tcs.SetException(exc); } return tcs.Task; } 


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; // something happens with task. oh no! string contentOnceAgain = await task; return content.Equals(contentOnceAgain); } } 

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:

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.
Actually code
 /// <summary>Gets a task that's already been completed successfully.</summary> /// <remarks>May not always return the same instance.</remarks> public static Task CompletedTask { get { var completedTask = s_completedTask; if (completedTask == null) s_completedTask = completedTask = new Task(false, (TaskCreationOptions)InternalTaskOptions.DoNotDispose, default(CancellationToken)); // benign initialization race condition return completedTask; } } 


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.

Source: https://habr.com/ru/post/280344/


All Articles