Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Are there any guidelines for writing .NET APIs using the new async/await features [closed]

I'm currently designing some internal APIs where I use the Async CTP and its new await/async keywords.

Are there any guidelines or best practices on how these APIs should be designed?

Specifically:

  • Should I provide both a synchronous and an asynchronous version of methods? (i.e. Task DoStuffAsync() and void DoStuff()
  • Should all async methods I expose be in the form async Task<T> GetStuffAsync() (i.e. method name end with Async) or is it ok to to have something named GetStuff() be awaitable?

I do understand that it's not all black or white here, and that it depends on the method in question, but I'm looking for general guidelines.

like image 745
Isak Savo Avatar asked Oct 26 '11 13:10

Isak Savo


People also ask

Should I always use async await C#?

This is important to keep in mind. If await is not used in the body of an async method, the C# compiler generates a warning, but the code compiles and runs as if it were a normal method. This is incredibly inefficient, as the state machine generated by the C# compiler for the async method is not accomplishing anything.

Is await blocking C#?

The await operator doesn't block the thread that evaluates the async method. When the await operator suspends the enclosing async method, the control returns to the caller of the method.

Which of the following is considered as the highly advantages feature of async await?

A significant benefit of the async/await pattern in languages that support it is that asynchronous, non-blocking code can be written, with minimal overhead, and looking almost like traditional synchronous, blocking code.

Does async await block execution?

Because await is only valid inside async functions and modules, which themselves are asynchronous and return promises, the await expression never blocks the main thread and only defers execution of code that actually depends on the result, i.e. anything after the await expression.


2 Answers

Hard to find the nuggets, most publications are focused on the client programming side. Good stuff is:

  • A white paper titled The Task-based Asynchronous Pattern. Download is here (note: slow to respond this morning).
  • Stephen Toub had a presentation at Windows Build that talked about optimizing the pattern, many important hints. There's a video, also available in print.

Do keep in mind this is all preview, important details may change before this ships.

like image 174
Hans Passant Avatar answered Oct 13 '22 00:10

Hans Passant


One crucial thing to use when using await to implement async APIs is to make sure to use ConfigureAwait(false) whenever you want to await a task inside the API impl. What that does is allow for the TPL to schedule your await resumption using the TPL default behavior (threadpool), rather than the TaskAwaiter's default behavior (current sync context).

Using the current sync context is the right default behavior for consumers because it allows for things such as await returning to the UI thread, if you were already on the UI thread. However, trying to come back to the UI thread can have problems if the UI thread isn't available to execute the rest of the method. The way await gets threads to execute the method is the standard .NET convention of creating delegates under the hood. These delegates then get sent to be processed in whatever sort of dispatching mechanism (e.g. WinForms message pump, WPF dispatcher, or anything else).

However, trying to come back to the same context is typically the wrong thing for API implementations, because that implicitly takes a dependency on that original context being available for execution.

For example, if I have some code on the UI thread:

void MyUIThreadCode() {
    Task asyncTask = MyAsyncMethod();
    asyncTask.Wait();
}

async Task MyAsyncMethod() {
    await DownloadSomethingAsync();
    ComputeSomethingElse();
}

This kind of code is [b]very[/b] tempting to write, and very easy to cause hangs. The typical case is that inside MyAsyncMethod(), there is an await that uses default sync context scheduling. That means that in the UI context, the DownloadSomethingAsync() method will get called, and the downloading will begin.

MyAsyncMethod() then tests if the await operand is "done". Let's say that it isn't done downloading, so then the defined behavior for await is to carve away the "rest" of the method, and schedule that for execution once the await operand really is done.

So... the state for executing the rest of the method gets stashed away in a delegate, and now MyAsyncMethod() returns back its own task to MyUIThreadCode().

Now MyUIThreadCode() calls Task.Wait() on the returned task. But the issue is that Task in .NET is really a general purpose representation of anything that has the notion of "done-ness". Just because you have a Task object, there is nothing to guarantee how it's going to execute, nor how is it going to reach completion. If you're guessing, another thing that is not guaranteed are its implicit dependencies.

So in the above example, MyAsyncMethod() uses the default await behavior on a Task, which schedules method continuations on the current context. The method continuation needs to execute before MyAsyncMethod()'s returned task gets considered completed.

However, MyUIThreadCode() called Wait() on the task. The defined behavior there is to BLOCK the current thread, keep the current function on the stack, and effectively wait until the task is done.

What the user didn't realize here was that the task they are blocked on relies on the UI thread actively processing, which it can't do because it's still busy executing the function that is blocked on the Wait() call.

  1. In order for MyUIThreadCode() to finish its method call, it needs for Wait() to return (2)
  2. In order for Wait() to return, it needs asyncTask to complete (3).
  3. In order for asyncTask to complete, it needs for the method continuation from MyAsyncMethod() to be executed (4).
  4. In order for the method continuation to get run, it needs the message loop to be processing (5).
  5. In order for the message loop to continue processing, it needs MyUIThreadCode() to return (1).

There's the circular dependency spelled out, none of the conditions end up being satisfied, and effectively the UI thread hangs.

Here's how you fix it with ConfigureAwait(false):

void MyUIThreadCode() {
    Task asyncTask = MyAsyncMethod();
    asyncTask.Wait();
}

async Task MyAsyncMethod() {
    await DownloadSomethingAsync().ConfigureAwait(false);
    ComputeSomethingElse();
}

What happens here is that the method continuation for MyAsyncMethod() uses the TPL default (threadpool), rather than the current sync context. And here are the conditions now, with that behavior:

  1. In order for MyUIThreadCode() to finish its method call, it needs for Wait() to return (2)
  2. In order for Wait() to return, it needs asyncTask to complete (3).
  3. In order for asyncTask to complete, it needs for the method continuation from MyAsyncMethod() to be executed (4).
  4. (NEW) In order for the method continuation to get run, it needs the thread pool to be processing (5).
  5. Effectively, the threadpool is always processing. In fact, the .NET threadpool is very much tuned to have high scalability (dynamically allocates and retires threads), as well as low latency (it has a max threshold that it allows for a request to get stale, before it goes ahead and starts a new thread to ensure throughput is preserved).

You're making a bet on .NET being a solid platform already, and we take the thread pool very seriously in .NET.

So, one might ask perhaps the problem is with the Wait() call... why did they use a blocking wait to begin with?

The answer is that sometimes you really just have to. For example, the .NET contract for Main() methods is that the program is terminated when the Main() method returns. Or... in other words, the Main() method blocks until your program is done.

Other things like interface contracts, or virtual method contracts typically have specific promises that certain things are performed before that method returns. Unless your interface or virtual method gives back a Task... you'll likely need to do some blocking on any async APIs that are called. This effectively defeats the purpose of async in that one situation... but perhaps you benefit from the asynchrony in a different codepath.

Thus for API providers that return async Tasks, by using ConfigureAwait(false), you are helping ensure that your returned Tasks don't have any unexpected implicit dependencies (e.g. like the UI message loop is still actively pumping). The more you can contain your dependencies, the better an API you are.

Hope this helps!

like image 26
Theo Yaung Avatar answered Oct 12 '22 22:10

Theo Yaung