Chapter 4. Writing Async Methods
Now we know how great asynchronous code is, but how hard it is to
write? It’s time to look at the C# 5.0 async feature. As we saw previously
in What Async Does, a method marked async
is allowed to contain the await
keyword.
private
async
void
DumpWebPageAsync
(
string
uri
)
{
WebClient
webClient
=
new
WebClient
();
string
page
=
await
webClient
.
DownloadStringTaskAsync
(
uri
);
Console
.
WriteLine
(
page
);
}
The await
expression in this
example transforms the method, so it pauses during the download, then
resumes when the download is done. This transformation makes the method
asynchronous. In this chapter, we’ll explore writing async methods like this
one.
Converting the Favicon Example to Async
We’ll now modify the favicon browser example from earlier to make
use of async. If you can, open the original version of the example (the
default
branch) and try to convert it
by adding async
and await
keywords before reading any
further.
The important method is AddAFavicon
, which downloads the icon, then adds
it to the UI. We want to make this method asynchronous, so that the UI
thread is free to respond to user actions during the download. The first
step is to add the async
keyword to the
method. It appears in the method signature in the same way that the
static
keyword does.
Then, we need to wait for the download using the await
keyword. In terms of C# syntax, await
acts as a unary operator, like the
!
not operator, or the (
type
)
cast operator. It is placed to the left of an
expression and means to wait for that expression asynchronously.
Finally, the call to DownloadData
must be changed to instead call the asynchronous version, DownloadDataTaskAsync
.
Note
An async method isn’t automatically asynchronous. Async methods just make it easier to consume other asynchronous methods. They start running synchronously, until they call an asynchronous method and await it. When they do so, they necessarily become asynchronous themselves. Sometimes, an async method never awaits anything, in which case it runs synchronously.
private
async
void
AddAFavicon
(
string
domain
)
{
WebClient
webClient
=
new
WebClient
();
byte
[]
bytes
=
await
webClient
.
DownloadData
TaskAsync
(
"http://"
+
domain
+
"/favicon.ico"
);
Image
imageControl
=
MakeImageControl
(
bytes
);
m_WrapPanel
.
Children
.
Add
(
imageControl
);
}
Compare this to the other two versions of this code we’ve looked at. It looks much more like the original synchronous version of the code. There’s no extra method, just a little extra code in the same structure. However, it behaves much more like the asynchronous version that we wrote in Converting the Example to Use Manual Asynchronous Code.
Task and await
Let’s break down the await
expression we’ve written. Here is the signature of the WebClient.DownloadStringTaskAsync
method:
Task
<
string
>
DownloadStringTaskAsync
(
string
address
)
The return type is Task<string>
. As I said in An Introduction to Task, a Task
represents an ongoing operation, and its
subclass Task<
represents an operation that will have a result of type
T
>T
at some point in the future. You can think of
Task<
as a
promise of a T
>T
when the long-running operation
completes.
Task
and Task<
can
both represent asynchronous operations, and both have the ability to call
back your code when the operation is done. To use that ability manually,
you use their T
>ContinueWith
methods to
pass a delegate containing the code to execute when the long-running
operation is done. await
uses the same
ability to execute the rest of your async method in the same way.
If you apply await
to a Task<
, it
becomes an await expression, and the whole
expression has type T
>T
. That means you can
assign the result of awaiting to a variable and use it in the rest of the
method, as we’ve seen in the examples. However, when you await a
non-generic Task
, it becomes an
await statement, and can’t be assigned to anything,
just like a call to a void
method. This
makes sense, as a Task
doesn’t promise
a result value, it only represents the operation itself.
await
smtpClient
.
SendMailAsync
(
mailMessage
);
There is nothing stopping us from splitting up the await
expression, so we can access the Task
directly, or do something else, before
awaiting it.
Task
<
string
>
myTask
=
webClient
.
DownloadStringTaskAsync
(
uri
);
// Do something here
string
page
=
await
myTask
;
It is important to fully understand the implications of this. The
method DownloadStringTaskAsync
is
executed on the first line. It begins executing synchronously, in the
current thread, and once it has started the download, it returns a
Task<string>
, still in the
current thread. It’s only later when we await that Task<string>
that the compiler does
something special. This is all still true if you write the await
on the same line as the call to the
asynchronous method.
The long-running operation starts as soon as the call to DownloadStringTaskAsync
is made, which gives us
a very simple way to perform multiple asynchronous operations
concurrently. We can just start multiple operations, keeping all the
Task
s, then await them all
afterwards.
Task
<
string
>
firstTask
=
webClient1
.
DownloadStringTaskAsync
(
"http://oreilly.com"
);
Task
<
string
>
secondTask
=
webClient2
.
DownloadStringTaskAsync
(
"http://simple-talk.com"
);
string
firstPage
=
await
firstTask
;
string
secondPage
=
await
secondTask
;
Note
This is a dangerous way to await multiple Task
s, if they may throw exceptions. If both operations throw an
exception, the first await
will
propagate its exception, which means secondTask
is never awaited. Its exception
will not be observed, and depending on .NET version and settings, may be
lost or even rethrown on an unexpected thread, terminating the process.
We’ll see better ways to do this in Chapter 7.
Async Method Return Types
There are three return types that a method marked async
may have:
void
Task
Task<
for some typeT
>T
No other return type is allowed because async methods in general aren’t finished when they return. Typically, an async method will await a long-running operation, which means that the method returns quickly, but will resume in the future. That means no sensible result value is available when the method returns. The result will be available later.
Note
I’ll make the distinction between the return type of a method—for example, Task<string>
—and the result type that the programmer actually
intends to give to the caller, which in this case is string
. In normal non-async methods, the
return type and the result type are always the same, but the difference
is important for async methods.
It’s obvious that void
is a
reasonable choice of return type in an asynchronous situation. A async void
method is a “fire and forget”
asynchronous operation. The caller can never wait for any result, and
can’t know when the operation completes or whether it was successful. You
should use void
when you know that no
caller will ever need to know when the operation is finished or whether it
succeeded. In practice, this means that void
is used very rarely. The most common use of
async void
methods is in the boundary
between async code and other code, for example a UI event handler must
return void
.
Async methods that return Task
allow the caller to wait for the operation to finish, and propagate any
exception that happened during the asynchronous operation. When no result
value is needed, an async Task
method
is better than an async void
method
because it allows the caller to also use await
to wait for it, making ordering and
exception handling easier.
Finally, async methods that return Task<
, for
example T
>Task<string>
, are used
when the asynchronous operation has a result value.
Async, Method Signatures, and Interfaces
The async
keyword appears in the
declaration of a method, just like the public
or static
keywords do. Despite that, async
is not
part of the signature of the method, in terms of overriding other methods,
implementing interfaces, or being called.
The only effect that the async
keyword has is on the compilation of the method to which it is applied,
unlike the other keywords that are applied to a method, which change how
it interacts with the outside world. Because of this, the rules around
overriding methods and implementing interfaces completely ignore the
async
keyword.
class
BaseClass
{
public
virtual
async
Task
<
int
>
AlexsMethod
()
{
...
}
}
class
SubClass
:
BaseClass
{
// This overrides AlexsMethod above
public
override
Task
<
int
>
AlexsMethod
()
{
...
}
}
Interfaces can’t use async
in a
method declaration, simply because there is no need. If an interface
requires that a method returns Task
,
the implementation may choose to use async
, but whether it does or not is a choice
for the implementing method. The interface doesn’t need to specify whether
to use async
or not.
The return Statement in Async Methods
The return
statement has
different behavior in an async method. Remember that in a normal non-async
method, use of the return
statement
depends on the return type of the method:
void
methodsreturn
statements must just bereturn;
, and are optional- Methods that return a type
T
return
must have an expression of typeT
(for examplereturn 5+x;
) and must exist at the end of the method on all code paths
In a method marked async
, the
rules apply in different situations:
void
methods and methods that returnTask
return
statements must just bereturn;
and are optional- Methods that return
Task<
T
> return
must have an expression of typeT
and must exist at the end of the method on all code paths
In async methods, the return type of the method is different from
the type of the expression found in the return
statement. The compiler transformation
can be thought to wrap up the value you return in a Task<
before
giving it to the caller. Of course, in reality, the T
>Task<
is
created immediately, and only filled with your result value later, once
any long-running operation is done.T
>
Async Methods Are Contagious
As we’ve seen, the best way to consume a Task
returned by an asynchronous API is to await
it in an async method. When you do this, your method will typically return
Task
as well. To get the benefit of the
asynchronous style, the code that calls your method must not block waiting
for your Task
to complete, and so your
caller will probably also await you.
Here’s an example of a helper method I’ve written that gets the number of characters on a web page, and returns them asynchronously.
private
async
Task
<
int
>
GetPageSizeAsync
(
string
url
)
{
WebClient
webClient
=
new
WebClient
();
string
page
=
await
webClient
.
DownloadStringTaskAsync
(
url
);
return
page
.
Length
;
}
To use it, I need to write another async method, which returns its result asynchronously as well:
private
async
Task
<
string
>
FindLargestWebPage
(
string
[]
urls
)
{
string
largest
=
null
;
int
largestSize
=
0
;
foreach
(
string
url
in
urls
)
{
int
size
=
await
GetPageSizeAsync
(
url
);
if
(
size
>
largestSize
)
{
size
=
largestSize
;
largest
=
url
;
}
}
return
largest
;
}
In this way, we end up writing chains of async methods, each awaiting the next. Async is a contagious programming model, and it can easily pervade a whole codebase. But I think that because async methods are so easy to write, this isn’t a problem at all.
Async Anonymous Delegates and Lambdas
Ordinary named methods can be async, and the two forms of anonymous methods can equally be async. The syntax is very much like normal methods. Here is how to make an asynchronous anonymous delegate:
Func
<
Task
<
int
>>
getNumberAsync
=
async
delegate
{
return
3
;
};
And here is an async lambda:
Func
<
Task
<
string
>>
getWordAsync
=
async
()
=>
"hello"
;
All the same rules apply in these as in ordinary async methods. You can use them to keep code concise, and to capture closures, in exactly the same way you would in non-async code.
Get Async in C# 5.0 now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.