Rules for working with the Tasks API. Part 2

    In this post I would like to talk about at times a misunderstanding of the concept of tasks. I will also try to show some non-obviousness when working with TaskCompletionSource and simply completed tasks, their solution and sources.

    Problem


    Let us have some code:
    static Task ComputeAsync(Func highCpuFunc)
    {
        var tcs = new TaskCompletionSource();
        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");
    


    Are there any problems with the code above along with an example? If so, which ones? It seems to be catching an AggregateException. Everything is gonna be ok ?

    NB : the topic of cancellation of tasks will be revealed next time, therefore we will not consider the absence of a cancellation token.

    The origins


    The concept of tasks is very closely related to the idea of ​​asynchrony, which is sometimes confused with multithreaded execution. And this, in turn, leads to the conclusion that each call is a task - something running somewhere out 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 just be Task.FromResult, for example.

    So, problem No. 1 is in the use case: you need to catch an InvalidOperationException (why it will become obvious a little lower) or any other exception along with an AggregateException.
    Task.WhenAll and co. The methods are documented as throws AggregateException, ArgumentNullException, ObjectDisposedException- this is true.

    But you should understand the order of code execution: if the body of ComputeAsync started to be executed in the calling thread, then the matter will not reach Task.WhenAll. Although this is a little and not obvious.

    The correct option:
    try
    {
        Task.WaitAll(ComputeAsync(() =>
        {
            // do work
        }));
    }
    catch (AggregateException)
    {
    }
    catch (InvalidOperationException)
    {
    }
    Console.WriteLine("Everything is gonna be ok");
    


    OK, sorted it out. Move on.

    The API itself provided by the TaskCompletionSource class is very intuitive. The methods SetResult , SetCanceled , SetException speak for themselves. But here lies the problem: they manipulate the state of the total task.

    Hmm ... Already got the trick? Let's consider in more detail.

    The ComputeAsync method has a section of code where SetResult is set, changing the state of the task to RanToCompletion.
    After that, in line c evil code(as if hinting) if an exception is raised, it will be processed and captured in SetException, which will be an attempt No. 2 to change the state of the task.

    In this case, the state of the Task class itself is immutable .

    NB : Why is this behavior good? Consider an example:

    static async Task ReadContentTwice()
    {
        using (var stringReader = new StringReader("blabla"))
        {
            Task task = stringReader.ReadToEndAsync();
            string content = await task;
            // something happens with task. oh no!
            string contentOnceAgain = await task;
            return content.Equals(contentOnceAgain);
        }
    }
    

    If the task state 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 the issue of behavior is relevant).

    The rest is simple - InvalidOperationException and blah blah.

    Decision


    All very obvious cause SetResult right before exiting the method always .

    Ordered SetResult
    static Task ComputeAsync(Func highCpuFunc)
    {
        var tcs = new TaskCompletionSource();
        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 the methods TrySetResult , TrySetCanceled , TrySetException ?!

    To use these, you must answer the question:
    • Is the scope of using TaskCompletionSource itself limited to this method only?

    If the answer to the question above is NO, then be sure to use TryXXX. This includes patterns of APM, EAP.
    If the code is simple as in the original example - a simple ordering of methods.


    Bonus track


    Each time, invoking Task.FromResult is inefficient. Why waste your 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 . Moreover (you guessed it) there is some peculiarity .

    Let's start with the fresh: the new property, the Task.CompletedTask: property, is just a static property of the Task type (I want to note exactly what the non-generic option is). Well, OK. It is unlikely to come in handy, because seldom tusks are without result.

    And also ... the documentation says: May not always return the same instance . Made on purpose.
    Actually code
    /// Gets a task that's already been completed successfully.
    /// May not always return the same instance.        
    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;
        }
    }
    


    To never cache or compare with the value (i.e. reference) of Task.CompletedTask when checking for completed task.

    The solution to this problem is very simple:

    public static class CompletedTaskSource
    {
        private static readonly Task CompletedTask = Task.FromResult(default(T));
        public static Task Task
        {
            get
            {
                return CompletedTask;
            }
        }
    }
    


    And that’s all. Fortunately, for .NET 4, there is a separate Microsoft Async NuGet package that allows you to compile C # 5 code for .NET 4 + brings the missing Task.FromResult, etc.

    Also popular now: