Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
CSharpNotesForProfessionals.pdf
Скачиваний:
65
Добавлен:
20.05.2023
Размер:
6.12 Mб
Скачать

Chapter 114: Async-Await

In C#, a method declared async won't block within a synchronous process, in case of you're using I/O based operations (e.g. web access, working with files, ...). The result of such async marked methods may be awaited via the use of the awaitkeyword.

Section 114.1: Await operator and async keyword

await operator and async keyword come together:

The asynchronous method in which await is used must be modified by the async keyword.

The opposite is not always true: you can mark a method as async without using await in its body.

What await actually does is to suspend execution of the code until the awaited task completes; any task can be awaited.

Note: you cannot await for async method which returns nothing (void).

Actually, the word 'suspends' is a bit misleading because not only the execution stops, but the thread may become free for executing other operations. Under the hood, await is implemented by a bit of compiler magic: it splits a method into two parts - before and after await. The latter part is executed when the awaited task completes.

If we ignore some important details, the compiler roughly does this for you:

public async Task<TResult> DoIt()

{

//do something and acquire someTask of type Task<TSomeResult> var awaitedResult = await someTask;

//... do something more and produce result of type TResult return result;

}

becomes:

public Task<TResult> DoIt()

{

// ...

return someTask.ContinueWith(task => {

var result = ((Task<TSomeResult>)task).Result; return DoIt_Continuation(result);

});

}

private TResult DoIt_Continuation(TSomeResult awaitedResult)

{

// ...

}

Any usual method can be turned into async in the following way:

await Task.Run(() => YourSyncMethod());

This can be advantageous when you need to execute a long running method on the UI thread without freezing the

GoalKicker.com – C# Notes for Professionals

584

UI.

But there is a very important remark here: Asynchronous does not always mean concurrent (parallel or even multi-threaded). Even on a single thread, async-await still allows for asynchronous code. For example, see this custom task scheduler. Such a 'crazy' task scheduler can simply turn tasks into functions which are called within message loop processing.

We need to ask ourselves: What thread will execute the continuation of our method DoIt_Continuation?

By default the await operator schedules the execution of continuation with the current Synchronization context. It means that by default for WinForms and WPF continuation runs in the UI thread. If, for some reason, you need to change this behavior, use method Task.ConfigureAwait():

await Task.Run(() => YourSyncMethod()).ConfigureAwait(continueOnCapturedContext: false);

Section 114.2: Concurrent calls

It is possible to await multiple calls concurrently by first invoking the awaitable tasks and then awaiting them.

public async Task RunConcurrentTasks()

{

var firstTask = DoSomethingAsync();

var secondTask = DoSomethingElseAsync();

await firstTask; await secondTask;

}

Alternatively, Task.WhenAll can be used to group multiple tasks into a single Task, which completes when all of its passed tasks are complete.

public async Task RunConcurrentTasks()

{

var firstTask = DoSomethingAsync();

var secondTask = DoSomethingElseAsync();

await Task.WhenAll(firstTask, secondTask);

}

You can also do this inside a loop, for example:

List<Task> tasks = new List<Task>(); while (something) {

// do stuff

Task someAsyncTask = someAsyncMethod(); tasks.Add(someAsyncTask);

}

await Task.WhenAll(tasks);

To get results from a task after awaiting multiple tasks with Task.WhenAll, simply await the task again. Since the task is already completed it will just return the result back

var task1 = SomeOpAsync();

var task2 = SomeOtherOpAsync();

GoalKicker.com – C# Notes for Professionals

585

await Task.WhenAll(task1, task2); var result = await task2;

Also, the Task.WhenAny can be used to execute multiple tasks in parallel, like the Task.WhenAll above, with the di erence that this method will complete when any of the supplied tasks will be completed.

public async Task RunConcurrentTasksWhenAny()

{

var firstTask = TaskOperation("#firstTask executed"); var secondTask = TaskOperation("#secondTask executed"); var thirdTask = TaskOperation("#thirdTask executed"); await Task.WhenAny(firstTask, secondTask, thirdTask);

}

The Task returned by RunConcurrentTasksWhenAny will complete when any of firstTask, secondTask, or thirdTask completes.

Section 114.3: Try/Catch/Finally

Version ≥ 6.0

As of C# 6.0, the await keyword can now be used within a catch and finally block.

try {

var client = new AsyncClient(); await client.DoSomething();

} catch (MyException ex) {

await client.LogExceptionAsync(); throw;

} finally {

await client.CloseAsync();

}

Version ≥ 5.0 Version < 6.0

Prior to C# 6.0, you would need to do something along the lines of the following. Note that 6.0 also cleaned up the null checks with the Null Propagating operator.

AsynClient client; MyException caughtException; try {

client = new AsyncClient(); await client.DoSomething();

} catch (MyException ex) { caughtException = ex;

}

if (client != null) {

if (caughtException != null) { await client.LogExceptionAsync();

}

await client.CloseAsync();

if (caughtException != null) throw caughtException;

}

Please note that if you await a task not created by async (e.g. a task created by Task.Run), some debuggers may break on exceptions thrown by the task even when it is seemingly handled by the surrounding try/catch. This happens because the debugger considers it to be unhandled with respect to user code. In Visual Studio, there is an

GoalKicker.com – C# Notes for Professionals

586

option called "Just My Code", which can be disabled to prevent the debugger from breaking in such situations.

Section 114.4: Returning a Task without await

Methods that perform asynchronous operations don't need to use await if:

There is only one asynchronous call inside the method

The asynchronous call is at the end of the method

Catching/handling exception that may happen within the Task is not necessary

Consider this method that returns a Task:

public async Task<User> GetUserAsync(int id)

{

var lookupKey = "Users" + id;

return await dataStore.GetByKeyAsync(lookupKey);

}

If GetByKeyAsync has the same signature as GetUserAsync (returning a Task<User>), the method can be simplified:

public Task<User> GetUserAsync(int id)

{

var lookupKey = "Users" + id;

return dataStore.GetByKeyAsync(lookupKey);

}

In this case, the method doesn't need to be marked async, even though it's preforming an asynchronous operation. The Task returned by GetByKeyAsync is passed directly to the calling method, where it will be awaited.

Important: Returning the Task instead of awaiting it, changes the exception behavior of the method, as it won't throw the exception inside the method which starts the task but in the method which awaits it.

public Task SaveAsync()

{

try {

return dataStore.SaveChangesAsync();

}

catch(Exception ex)

{

// this will never be called logger.LogException(ex);

}

}

//Some other code calling SaveAsync()

//If exception happens, it will be thrown here, not inside SaveAsync() await SaveAsync();

This will improve performance as it will save the compiler the generation of an extra async state machine.

Section 114.5: Async/await will only improve performance if it allows the machine to do additional work

Consider the following code:

GoalKicker.com – C# Notes for Professionals

587