C #: one use case for any tasks

Original author: Sergey Teplyakov
  • Transfer
Hi, Habr! We continue to talk about asynchronous programming in C #. Today we will talk about a single use case or user scenario suitable for any tasks within asynchronous programming. We will touch on the topics of synchronization, deadlocks, operator settings, exception handling and much more. Join now!



Previous related articles


Virtually any nonstandard behavior of asynchronous methods in C # can be explained on the basis of a single user scenario: converting an existing synchronous code into asynchronous should be as simple as possible. You must be able to add the async keyword before the returned method type, add the Async suffix to the method name, and add the await keyword here and in the text area of ​​the method to get a fully functional asynchronous method.



The “simple” scenario drastically changes many aspects of the behavior of asynchronous methods: from planning the duration of a task to the exception handling. The script looks convincing and meaningful, but in its context the simplicity of asynchronous methods becomes very deceptive.

Synchronization context


User Interface Development (UI) is one of the areas where the scenario described above is particularly important. Due to the lengthy operations in the user interface flow, the response time of applications increases, and in this case asynchronous programming has always been considered a very effective tool.

privateasyncvoidbuttonOk_ClickAsync(object sender, EventArgs args)
{
    textBox.Text = "Running.."; // 1 -- UI Threadvar result = await _stockPrices.GetStockPricesForAsync("MSFT"); // 2 -- Usually non-UI Thread
    textBox.Text = "Result is: " + result; //3 -- Should be UI Thread
}

The code looks very simple, but one problem arises. For most user interfaces, there are limitations: UI elements can be modified only by special threads. That is, in line 3, an error occurs if the duration of the task is scheduled in the thread from the thread pool. Fortunately, this issue has been known for a long time, and the concept of synchronization context has appeared in the .NET Framework 2.0 version .

Each UI provides special utilities for marshaling tasks into one or more specialized user interface threads. Windows Forms uses the methodControl.Invoke, WPF - Dispatcher.Invoke, other systems can access other methods. The schemes used in all these cases are similar in many respects, but differ in details. The synchronization context allows you to abstract away the differences by providing an API for running code in a “special” context that provides processing of minor details with such derived types as WindowsFormsSynchronizationContext, DispatcherSynchronizationContext etc.

To solve the problem related to the similarity of threads, C # programmers decided to enter the current synchronization context at the initial stage of the implementation of asynchronous methods and to plan all subsequent operations in such a context. Now each of the blocks between await statements is executed in the user interface thread, which makes it possible to implement the main script. However, this decision gave rise to a number of new problems.

Deadlocks


Let's look at a small, relatively simple piece of code. Are there any problems here?

// UI codeprivatevoidbuttonOk_Click(object sender, EventArgs args)
{
    textBox.Text = "Running..";
    var result = _stockPrices.GetStockPricesForAsync("MSFT").Result;
    textBox.Text = "Result is: " + result;
}
// StockPrices.dllpublic Task<decimal> GetStockPricesForAsync(string symbol)
{
    await Task.Yield();
    return42;
}

This code causes a deadlock . The user interface thread starts an asynchronous operation and waits synchronously for the result. However, the execution of the asynchronous method cannot be completed, since the second line GetStockPricesForAsync must be executed in the user interface thread that causes the deadlock.

You object that this problem is quite easy to solve. Yes indeed. It is necessary to prohibit all calls to the method Task.Resultor Task.Waitfrom the user interface code, but the problem can still occur if the component used by such code synchronously awaits the result of the user operation:

// UI codeprivatevoidbuttonOk_Click(object sender, EventArgs args)
{
    textBox.Text = "Running..";
    var result = _stockPrices.GetStockPricesForAsync("MSFT").Result;
    textBox.Text = "Result is: " + result;
}
// StockPrices.dllpublic Task<decimal> GetStockPricesForAsync(string symbol)
{
    // We know that the initialization step is very fast,// and completes synchronously in most cases,// let's wait for the result synchronously for "performance reasons".
    InitializeIfNeededAsync().Wait();
    return Task.FromResult((decimal)42);
}
// StockPrices.dllprivateasync Task InitializeIfNeededAsync() => await Task.Delay(1);

This code again causes deadlock. How to solve it:

  • The asynchronous snippet should be blocked, not by Task.Wait()or Task.Resultand
  • use ConfigureAwait(false)in library code.

The meaning of the first recommendation is clear, and the second we will explain below.

Configuring await statements


There are two reasons why a deadlock occurs in the last example: Task.Wait()in GetStockPricesForAsyncand indirectly using the synchronization context in subsequent steps in InitializeIfNeededAsync. Although C # programmers do not recommend blocking calls to asynchronous methods, it is clear that in most cases this blocking is still used. C # programmers will propose the following solution to the problem with deadlocks: Task.ConfigureAwait(continueOnCapturedContext:false).

Despite the strange appearance (if the method is called without a named argument, it does not mean anything at all), this function fulfills its function: it ensures that the execution will continue without the synchronization context.

public Task<decimal> GetStockPricesForAsync(string symbol)
{
    InitializeIfNeededAsync().Wait();
    return Task.FromResult((decimal)42);
}
privateasync Task InitializeIfNeededAsync() => await Task.Delay(1).ConfigureAwait(false);

In this case, the continuation of the task Task.Delay(1(here - empty operator) is planned in the stream from the thread pool, and not in the user interface thread, which eliminates deadlock.

Disable sync context


I know that ConfigureAwait actually solves this problem, but it itself generates much more. Here is a small example:

public Task<decimal> GetStockPricesForAsync(string symbol)
{
    InitializeIfNeededAsync().Wait();
    return Task.FromResult((decimal)42);
}
privateasync Task InitializeIfNeededAsync()
{
    // Initialize the cache field firstawait _cache.InitializeAsync().ConfigureAwait(false);
    // Do some workawait Task.Delay(1);
}

Do you see the problem? We used ConfigureAwait(false), so everything should be fine. But not a fact.

ConfigureAwait(false)returns a custom awaiter object ConfiguredTaskAwaitable, and we know that it is used only if the task does not complete synchronously. That is, if it _cache.InitializeAsync()terminates synchronously, a deadlock is still possible.

To eliminate a deadlock, all tasks awaiting completion need to be decorated with a method call ConfigureAwait(false). All this is annoying and generates errors.

Alternatively, you can use a custom awaiter object in all public methods to disable the synchronization context in the asynchronous method:

privatevoidbuttonOk_Click(object sender, EventArgs args)
{
    textBox.Text = "Running..";
    var result = _stockPrices.GetStockPricesForAsync("MSFT").Result;
    textBox.Text = "Result is: " + result;
}
// StockPrices.dllpublicasync Task<decimal> GetStockPricesForAsync(string symbol)
{
    // The rest of the method is guarantee won't have a current sync context.await Awaiters.DetachCurrentSyncContext();
    // We can wait synchronously here and we won't have a deadlock.
    InitializeIfNeededAsync().Wait();
    return42;
}

Awaiters.DetachCurrentSyncContext returns the following custom awaiter object:

publicstruct DetachSynchronizationContextAwaiter : ICriticalNotifyCompletion
{
    ///<summary>/// Returns true if a current synchronization context is null./// It means that the continuation is called only when a current context/// is presented.///</summary>publicbool IsCompleted => SynchronizationContext.Current == null;
    publicvoidOnCompleted(Action continuation)
    {
        ThreadPool.QueueUserWorkItem(state => continuation());
    }
    publicvoidUnsafeOnCompleted(Action continuation)
    {
        ThreadPool.UnsafeQueueUserWorkItem(state => continuation(), null);
    }
    publicvoidGetResult() { }
    public DetachSynchronizationContextAwaiter GetAwaiter() => this;
}
publicstaticclassAwaiters
{
    publicstatic DetachSynchronizationContextAwaiter DetachCurrentSyncContext()
    {
        returnnew DetachSynchronizationContextAwaiter();
    }
}

DetachSynchronizationContextAwaiterdoes the following: the async method works with a non-zero synchronization context. But if the async method works without a synchronization context, the property IsCompletedreturns true, and the method continues to run synchronously.

This means the presence of service data close to zero, when the asynchronous method is executed from a stream in the thread pool, and the payment is made once for transferring the execution from the user interface stream to the stream from the thread pool.

The following are other benefits of this approach.

  • Reduced probability of error. ConfigureAwait(false)only works if applied to all tasks awaiting completion. It is worth forgetting at least one thing - and a deadlock may occur. In the case of a custom awaiter object, remember that all public library methods must begin with Awaiters.DetachCurrentSyncContext(). Errors are possible here, but their probability is much lower.
  • The resulting code is more declarative and clear. The multi-call method ConfigureAwait seems to me less readable (because of the extra elements) and not informative enough for newbies.

Exception Handling


What is the difference between these two options:

Task mayFail = Task.FromException (new ArgumentNullException ());

// Case 1try { await mayFail; }
catch (ArgumentException e)
{
    // Handle the error
}
// Case 2try { mayFail.Wait(); }
catch (ArgumentException e)
{
    // Handle the error
}

In the first case, everything meets expectations - error handling is performed, but in the second case this does not happen. The TPL parallel task library is designed for asynchronous and parallel programming, and Task / Task can represent the result of several operations. That is why Task.Resultand Task.Wait()always give an exception AggregateException, which may contain several errors.

However, our main script changes everything: the user should be able to add an async / await statement, without touching the logic of error handling. That is, the await operator must be different from Task.Result/ Task.Wait(): it must remove the wrapper from one exception in the instance AggregateException. Today we will choose the first exception.

Everything is fine, if all Task-based methods are asynchronous and parallel computing is not used to perform tasks. But in some cases, everything is different:

try
{
    Task<int> task1 = Task.FromException<int>(new ArgumentNullException());
    Task<int> task2 = Task.FromException<int>(new InvalidOperationException());
    // await will rethrow the first exceptionawait Task.WhenAll(task1, task2);
}
catch (Exception e)
{
    // ArgumentNullException. The second error is lost!
    Console.WriteLine(e.GetType());
}

Task.WhenAllreturns a task with two errors, however the await operator retrieves and fills only the first one.

There are two ways to solve this problem:

  1. manually view tasks if they are accessible, or
  2. configure the TPL library to force the exception to be wrapped in another exception AggregateException.

try
{
    Task<int> task1 = Task.FromException<int>(new ArgumentNullException());
    Task<int> task2 = Task.FromException<int>(new InvalidOperationException());
    // t.Result forces TPL to wrap the exception into AggregateExceptionawait Task.WhenAll(task1, task2).ContinueWith(t => t.Result);
}
catch(Exception e)
{
    // AggregateException
    Console.WriteLine(e.GetType());
}

Async void method


The Task based method returns a token that can be used to process results in the future. If the task is lost, the token becomes inaccessible for reading by the user code. An asynchronous operation that returns a void method produces an error that cannot be processed in user code. In this sense, tokens are useless and even dangerous - now we will see it. However, our main scenario assumes their mandatory use:

privateasyncvoidbuttonOk_ClickAsync(object sender, EventArgs args)
{
    textBox.Text = "Running..";
    var result = await _stockPrices.GetStockPricesForAsync("MSFT");
    textBox.Text = "Result is: " + result;
}

But what if it GetStockPricesForAsync gives an error? An unhandled async void method exception is marshaled to the current synchronization context, triggering the same behavior as for synchronous code (for more information, see the ThrowAsync Method section of the AsyncMethodBuilder.cs web page). In Windows Forms, an unhandled exception in the event handler triggers an event Application.ThreadException, for WPF, an event is triggered, Application.DispatcherUnhandledExceptionand so on.

And if the async void method does not receive a synchronization context? In this case, an unhandled exception causes a fatal application crash. It will not trigger the event being restored [ TaskScheduler.UnobservedTaskException], but will trigger the unrecoverable event AppDomain.UnhandledExceptionand then close the application. This is intentional, and this is the result we need.

Now let's consider another well-known way: using asynchronous void methods only for UI event handlers.

Unfortunately, the asynch void method is easy to call by accident.

publicstatic Task<T> ActionWithRetry<T>(Func<Task<T>> provider, Action<Exception> onError)
{
    // Calls 'provider' N times and calls 'onError' in case of an error.
}
publicasync Task<string> AccidentalAsyncVoid(string fileName)
{
    returnawait ActionWithRetry(
        provider:
        () =>
        {
            return File.ReadAllTextAsync(fileName);
        },
        // Can you spot the issue?
        onError:
        async e =>
        {
            await File.WriteAllTextAsync(errorLogFile, e.ToString());
        });
}

At first glance, it is difficult to say if a function is a Task-based method or an async void method, and therefore an error can creep into your code base, despite the most thorough check.

Conclusion


Many aspects of asynchronous programming in C # have been influenced by one user script — the simple conversion of the synchronous code of an existing user interface application to asynchronous:

  • Subsequent execution of asynchronous methods is scheduled in the received synchronization context, which can cause deadlocks.
  • To prevent them, it is necessary to place calls in the asynchronous library code everywhere ConfigureAwait(false).
  • await task; throws the first error, and this complicates the creation of a processing exception for parallel programming.
  • Async void methods are introduced to handle user interface events, but they are easy to execute completely by accident, which will cause the application to crash if there is an unhandled exception.

Free cheese is only in a mousetrap. Ease of use can sometimes result in great difficulty in other areas. If you are familiar with the history of asynchronous programming in C #, the strangest behavior no longer seems so strange, and the likelihood of errors in asynchronous code is significantly reduced.

Also popular now: