Asynchronous Functions (C# 5.0)

C# 5.0 introduces the await and async keywords to support asynchronous programming, a style of programming where long-running functions do most or all of their work after returning to the caller. This is in contrast to normal synchronous programming, where long-running functions block the caller until the operation is complete. Asynchronous programming implies concurrency, since the long-running operation continues in parallel to the caller. The implementer of an asynchronous function initiates this concurrency either through multithreading (for compute-bound operations) or via a callback mechanism (for I/O-bound operations).

Note

Multithreading, concurrency, and asynchronous programming are large topics. We dedicate two chapters to them in C# 5.0 in a Nutshell, and discuss them online at http://albahari.com/threading.

For instance, consider the following synchronous method, which is long-running and compute-bound:

int ComplexCalculation()
{
  double x = 2;
  for (int i = 1; i < 100000000; i++)
    x += Math.Sqrt (x) / i;
  return (int)x;
}

This method blocks the caller for a few seconds while it runs. The result of the calculation is then returned to the caller:

int result = ComplexCalculation();
// Sometime later:
Console.WriteLine (result);   // 116

The CLR defines a class called Task<TResult> (in System.Threading.Tasks) to encapsulate the concept of an operation that completes in the future. You can generate a Task<TResult> for a compute-bound operation by calling Task.Run, which tells the CLR to run the specified delegate on a separate thread that executes in parallel to the caller:

Task<int> ComplexCalculationAsync()
{
  return Task.Run (() => ComplexCalculation());
}

This method is asynchronous because it returns immediately to the caller while it executes concurrently. However, we need some mechanism to allow the caller to specify what should happen when the operation finishes and the result becomes available. Task<TResult> solves this by exposing a GetAwaiter method which lets the caller attach a continuation:

Task<int> task = ComplexCalculationAsync();
var awaiter = task.GetAwaiter();
awaiter.OnCompleted (() =>        // Continuation
{
  int result = awaiter.GetResult();
  Console.WriteLine (result);       // 116
});

This says to the operation, “When you finish, execute the specified delegate.” Our continuation first calls GetResult which returns the result of the calculation. (Or, if the task faulted [threw an exception], calling GetResult rethrows that exception.) Our continuation then writes out the result via Console.WriteLine.

The await and async Keywords

The await keyword simplifies the attaching of continuations. Starting with a basic scenario, the compiler expands:

var result = await expression;
statement(s);

into something functionally similar to:

var awaiter = expression.GetAwaiter();
awaiter.OnCompleted (() =>
{
  var result = awaiter.GetResult();
  statement(s);
);

Note

The compiler also emits code to optimize the scenario of the operation completing synchronously (immediately). The most common reason for an asynchronous operation completing immediately is if it implements an internal caching mechanism, and the result is already cached.

Hence, we can call the ComplexCalculationAsync method we defined previously, like this:

int result = await ComplexCalculationAsync();
Console.WriteLine (result);

In order to compile, we need to add the async modifier to the containing method:

async void Test()
{
  int result = await ComplexCalculationAsync();
  Console.WriteLine (result);
}

The async modifier tells the compiler to treat await as a keyword rather than an identifier should an ambiguity arise within that method (this ensures that code written prior to C# 5.0 that might use await as an identifier will still compile without error). The async modifier can be applied only to methods (and lambda expressions) that return void or (as we’ll see later) a Task or Task<TResult>.

Note

The async modifier is similar to the unsafe modifier in that it has no effect on a method’s signature or public metadata; it affects only what happens inside the method.

Methods with the async modifier are called asynchronous functions, because they themselves are typically asynchronous. To see why, let’s look at how execution proceeds through an asynchronous function.

Upon encountering an await expression, execution (normally) returns to the caller—rather like with yield return in an iterator. But before returning, the runtime attaches a continuation to the awaited task, ensuring that when the task completes, execution jumps back into the method and continues where it left off. If the task faults, its exception is rethrown (by virtue of calling GetResult); otherwise, its return value is assigned to the await expression.

Note

The CLR’s implementation of a task awaiter’s OnCompleted method ensures that by default, continuations are posted through the current synchronization context, if one is present. In practice, this means that in rich-client UI scenarios (WPF, Metro, Silverlight, and Windows Forms), if you await on a UI thread, your code will continue on that same thread. This simplifies thread safety.

The expression upon which you await is typically a task; however, any object with a GetAwaiter method that returns an awaitable object—implementing INotifyCompletion.OnCompleted and with an appropriately typed GetResult method (and a bool IsCompleted property which tests for synchronous completion)—will satisfy the compiler.

Notice that our await expression evaluates to an int type; this is because the expression that we awaited was a Task<int> (whose GetAwaiter().GetResult() method returns an int).

Awaiting a nongeneric task is legal and generates a void expression:

await Task.Delay (5000);
Console.WriteLine ("Five seconds passed!");

Task.Delay is a static method that returns a Task that completes in the specified number of milliseconds. The synchronous equivalent of Task.Delay is Thread.Sleep.

Task is the nongeneric base class of Task<TResult> and is functionally equivalent to Task<TResult> except that it has no result.

Capturing Local State

The real power of await expressions is that they can appear almost anywhere in code. Specifically, an await expression can appear in place of any expression (within an asynchronous function) except for inside a catch or finally block, a lock expression, an unsafe context, or an executable’s entry point (main method).

In the following example, we await inside a loop:

async void Test()
{
  for (int i = 0; i < 10; i++)
  {
    int result = await ComplexCalculationAsync();
    Console.WriteLine (result);
  }
}

Upon first executing ComplexCalculationAsync, execution returns to the caller by virtue of the await expression. When the method completes (or faults), execution resumes where it left off, with the values of local variables and loop counters preserved. The compiler achieves this by translating such code into a state machine, like it does with iterators.

Without the await keyword, the manual use of continuations means that you must write something equivalent to a state machine. This is traditionally what makes asynchronous programming difficult.

Writing Asynchronous Functions

With any asynchronous function, you can replace the void return type with a Task to make the method itself usefully asynchronous (and awaitable). No further changes are required:

async Task PrintAnswerToLife()
{
  await Task.Delay (5000);
  int answer = 21 * 2;
  Console.WriteLine (answer);
}

Notice that we don’t explicitly return a task in the method body. The compiler manufactures the task, which it signals upon completion of the method (or upon an unhandled exception). This makes it easy to create asynchronous call chains:

async Task Go()
{
  await PrintAnswerToLife();
  Console.WriteLine ("Done");
}

(And because Go returns a Task, Go itself is awaitable.) The compiler expands asynchronous functions that return tasks into code that (indirectly) leverages TaskCompletionSource to create a task that it then signals or faults.

Note

TaskCompletionSource is a CLR type that lets you create tasks that you manually control, signaling them as complete with a result (or as faulted with an exception). Unlike Task.Run, TaskCompletionSource doesn’t tie up a thread for the duration of the operation. It’s also used for writing I/O-bound task-returning methods (such as Task.Delay).

The aim is to ensure that when a task-returning asynchronous method finishes, execution can jump back to whoever awaited it, via a continuation.

Returning Task<TResult>

You can return a Task<TResult> if the method body returns TResult:

async Task<int> GetAnswerToLife()
{
  await Task.Delay (5000);
  int answer = 21 * 2;
  // answer is int so our method returns Task<int>
  return answer;
}

We can demonstrate GetAnswerToLife by calling it from PrintAnswerToLife (which is, in turn, called from Go):

async Task Go()
{
  await PrintAnswerToLife();
  Console.WriteLine ("Done");
}
async Task PrintAnswerToLife()
{
  int answer = await GetAnswerToLife();
  Console.WriteLine (answer);
}
async Task<int> GetAnswerToLife()
{
  await Task.Delay (5000);
  int answer = 21 * 2;
  return answer;
}

Asynchronous functions make asynchronous programming similar to synchronous programming. Here’s the synchronous equivalent of our call graph, for which calling Go() gives the same result after blocking for five seconds:

void Go()
{
  PrintAnswerToLife();
  Console.WriteLine ("Done");
}
void PrintAnswerToLife()
{
  int answer = GetAnswerToLife();
  Console.WriteLine (answer);
}
int GetAnswerToLife()
{
  Thread.Sleep (5000);
  int answer = 21 * 2;
  return answer;
}

This also illustrates the basic principle of how to design with asynchronous functions in C#, which is to write your methods synchronously, and then replace synchronous method calls with asynchronous method calls, and await them.

Parallelism

We’ve just demonstrated the most common pattern, which is to await task-returning functions right after calling them. This results in sequential program flow that’s logically similar to the synchronous equivalent.

Calling an asynchronous method without awaiting it allows the code that follows to execute in parallel. For example, the following executes PrintAnswerToLife twice, concurrently:

var task1 = PrintAnswerToLife();
var task2 = PrintAnswerToLife();
await task1; await task2;

By awaiting both operations afterward, we “end” the parallelism at that point (and rethrow any exceptions from those tasks). The Task class provides a static method called WhenAll to achieve the same result slightly more efficiently. WhenAll returns a task that completes when all of the tasks that you pass to it complete:

await Task.WhenAll (PrintAnswerToLife(),
                    PrintAnswerToLife());

WhenAll is a called task combinator. (The Task class also provides a task combinator called WhenAny, which completes when any of the tasks provided to it complete.) We cover the task combinators in detail in C# 5.0 in a Nutshell.

Asynchronous Lambda Expressions

Just as ordinary named methods can be asynchronous:

async Task NamedMethod()
{
  await Task.Delay (1000);
  Console.WriteLine ("Foo");
}

so can unnamed methods (lambda expressions and anonymous methods), if preceded by the async keyword:

Func<Task> unnamed = async () =>
{
  await Task.Delay (1000);
  Console.WriteLine ("Foo");
};

We can call and await these in the same way:

await NamedMethod();
await unnamed();

Asynchronous lambda expressions can be used when attaching event handlers:

myButton.Click += async (sender, args) =>
{
  await Task.Delay (1000);
  myButton.Content = "Done";
};

This is more succinct than the following, which has the same effect:

myButton.Click += ButtonHandler;
...
async void ButtonHander (object sender, EventArgs args)
{
  await Task.Delay (1000);
  myButton.Content = "Done";
};

Asynchronous lambda expressions can also return Task<TResult>:

Func<Task<int>> unnamed = async () =>
{
  await Task.Delay (1000);
  return 123;
};
int answer = await unnamed();
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.179.225