Previous related articles
')
private async void buttonOk_ClickAsync(object sender, EventArgs args) { textBox.Text = "Running.."; // 1 -- UI Thread var result = await _stockPrices.GetStockPricesForAsync("MSFT"); // 2 -- Usually non-UI Thread textBox.Text = "Result is: " + result; //3 -- Should be UI Thread }
Control.Invoke
method, 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 handling of minor details with such derived types as WindowsFormsSynchronizationContext
, DispatcherSynchronizationContext
, etc. // UI code private void buttonOk_Click(object sender, EventArgs args) { textBox.Text = "Running.."; var result = _stockPrices.GetStockPricesForAsync("MSFT").Result; textBox.Text = "Result is: " + result; } // StockPrices.dll public Task<decimal> GetStockPricesForAsync(string symbol) { await Task.Yield(); return 42; }
GetStockPricesForAsync
must be executed in the user interface thread that causes the deadlock.Task.Result
or Task.Wait
from the user interface code, however, the problem can still occur if the component used by such code synchronously waits for the result of the user operation: // UI code private void buttonOk_Click(object sender, EventArgs args) { textBox.Text = "Running.."; var result = _stockPrices.GetStockPricesForAsync("MSFT").Result; textBox.Text = "Result is: " + result; } // StockPrices.dll public 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.dll private async Task InitializeIfNeededAsync() => await Task.Delay(1);
Task.Wait()
or Task.Result
andConfigureAwait(false)
in library code.Task.Wait()
in GetStockPricesForAsync
and indirectly using the synchronization context at subsequent stages 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 offer the following solution to a deadlock problem: Task.ConfigureAwait(continueOnCapturedContext:false)
. public Task<decimal> GetStockPricesForAsync(string symbol) { InitializeIfNeededAsync().Wait(); return Task.FromResult((decimal)42); } private async Task InitializeIfNeededAsync() => await Task.Delay(1).ConfigureAwait(false);
Task.Delay(1
) task (here, an empty statement) is scheduled in the stream from the thread pool, and not in the user interface thread, which eliminates the deadlock.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); } private async Task InitializeIfNeededAsync() { // Initialize the cache field first await _cache.InitializeAsync().ConfigureAwait(false); // Do some work await Task.Delay(1); }
ConfigureAwait(false)
, so everything should be fine. But not a fact.ConfigureAwait(false)
returns a custom awaiter ConfiguredTaskAwaitable
object, and we know that it is used only if the task does not complete synchronously. That is, if _cache.InitializeAsync()
completes synchronously, a deadlock is still possible.ConfigureAwait(false)
method ConfigureAwait(false)
. All this is annoying and causes mistakes. private void buttonOk_Click(object sender, EventArgs args) { textBox.Text = "Running.."; var result = _stockPrices.GetStockPricesForAsync("MSFT").Result; textBox.Text = "Result is: " + result; } // StockPrices.dll public async 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(); return 42; }
Awaiters.DetachCurrentSyncContext
returns the following custom awaiter object: public struct 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> public bool IsCompleted => SynchronizationContext.Current == null; public void OnCompleted(Action continuation) { ThreadPool.QueueUserWorkItem(state => continuation()); } public void UnsafeOnCompleted(Action continuation) { ThreadPool.UnsafeQueueUserWorkItem(state => continuation(), null); } public void GetResult() { } public DetachSynchronizationContextAwaiter GetAwaiter() => this; } public static class Awaiters { public static DetachSynchronizationContextAwaiter DetachCurrentSyncContext() { return new DetachSynchronizationContextAwaiter(); } }
DetachSynchronizationContextAwaiter
does the following: the async method works with a non-zero synchronization context. But if the async method works without a synchronization context, the IsCompleted
property returns true, and the method continues to run synchronously.ConfigureAwait(false)
works only if it applies to all tasks waiting to be completed. 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.ConfigureAwait
seems to me less convenient for reading (due to unnecessary elements) and not informative enough for beginners. // Case 1 try { await mayFail; } catch (ArgumentException e) { // Handle the error } // Case 2 try { mayFail.Wait(); } catch (ArgumentException e) { // Handle the error }
Task.Result
and Task.Wait()
always throw an AggregateException
exception, which may contain several errors.Task.Result
/ Task.Wait()
: it must remove the wrapper from one exception in an AggregateException
instance. Today we will choose the first exception. try { Task<int> task1 = Task.FromException<int>(new ArgumentNullException()); Task<int> task2 = Task.FromException<int>(new InvalidOperationException()); // await will rethrow the first exception await Task.WhenAll(task1, task2); } catch (Exception e) { // ArgumentNullException. The second error is lost! Console.WriteLine(e.GetType()); }
Task.WhenAll
returns a task with two errors, but the await operator retrieves and fills only the first one.AggregateException
exception. 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 AggregateException await Task.WhenAll(task1, task2).ContinueWith(t => t.Result); } catch(Exception e) { // AggregateException Console.WriteLine(e.GetType()); }
private async void buttonOk_ClickAsync(object sender, EventArgs args) { textBox.Text = "Running.."; var result = await _stockPrices.GetStockPricesForAsync("MSFT"); textBox.Text = "Result is: " + result; }
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 on the AsyncMethodBuilder.cs web page). In Windows Forms, an unhandled exception in the event handler triggers the Application.ThreadException
event, for the WPF, the Application.DispatcherUnhandledException
event is fired, and so on.TaskScheduler.UnobservedTaskException
] event being restored, but will trigger the unrecoverable AppDomain.UnhandledException
event and then close the application. This is intentional, and this is the result we need. public static Task<T> ActionWithRetry<T>(Func<Task<T>> provider, Action<Exception> onError) { // Calls 'provider' N times and calls 'onError' in case of an error. } public async Task<string> AccidentalAsyncVoid(string fileName) { return await ActionWithRetry( provider: () => { return File.ReadAllTextAsync(fileName); }, // Can you spot the issue? onError: async e => { await File.WriteAllTextAsync(errorLogFile, e.ToString()); }); }
ConfigureAwait(false)
calls ConfigureAwait(false)
everywhere in the asynchronous library code.Source: https://habr.com/ru/post/421245/
All Articles