(Post 20/11/2007) In Part 1 we described how
to pass data to a thread, using ParameterizedThreadStart. Sometimes you
need to go the other way, and get return values back from a thread when
it finishes executing. Asynchronous delegates offer a convenient mechanism
for this, allowing any number of typed arguments to be passed in both
directions. Furthermore, unhandled exceptions on asynchronous delegates
are conveniently re-thrown on the original thread, and so don't need explicit
handling. Asynchronous delegates also provide another way into the thread
pool.
The price you must pay for all this is in following its
asynchronous model. To see what this means, we'll first discuss the more
usual, synchronous, model of programming. Let's say we want to compare
two web pages. We could achieve this by downloading each page in sequence,
then comparing their output as follows:
static void ComparePages()
{
WebClient wc = new WebClient ();
string s1 = wc.DownloadString ("http://www.oreilly.com");
string s2 = wc.DownloadString ("http://oreilly.com");
Console.WriteLine (s1 == s2 ? "Same" : "Different");
}
Of course it would be faster if both pages downloaded
at once. One way to view the problem is to blame DownloadString for blocking
the calling method while the page is downloading. It would be nice if
we could call DownloadString in a non-blocking asynchronous fashion, in
other words:
-
We tell DownloadString to start executing.
-
We perform other tasks while it's working, such
as downloading another page.
-
We ask DownloadString for its results.
The WebClient class
actually offers a built-in method called DownloadStringAsync which
provides asynchronous-like functionality. For now, we'll ignore
this and focus on the mechanism by which any method can be called
asynchronously. |
The third step is what makes asynchronous delegates useful.
The caller rendezvous with the worker to get results and to allow any
exception to be re-thrown. Without this step, we have normal multithreading.
While it's possible to use asynchronous delegates without the rendezvous,
you gain little over calling ThreadPool.QueueWorkerItem or using BackgroundWorker.
Here's how we can use asynchronous delegates to download
two web pages, while simultaneously performing a calculation:
delegate string DownloadString
(string uri);
static void ComparePages() {
// Instantiate delegates with DownloadString's signature:
DownloadString download1 = new WebClient().DownloadString;
DownloadString download2 = new WebClient().DownloadString;
// Start the downloads:
IAsyncResult cookie1 = download1.BeginInvoke (uri1, null, null);
IAsyncResult cookie2 = download2.BeginInvoke (uri2, null, null);
// Perform some random calculation:
double seed = 1.23;
for (int i = 0; i < 1000000; i++) seed = Math.Sqrt (seed + 1000);
// Get the results of the downloads, waiting for completion if necessary.
// Here's where any exceptions will be thrown:
string s1 = download1.EndInvoke (cookie1);
string s2 = download2.EndInvoke (cookie2);
Console.WriteLine (s1 == s2 ? "Same" : "Different");
}
We start by declaring and instantiating delegates for
methods we want to run asynchronously. In this example, we need two delegates
so that each can reference a separate WebClient object (WebClient does
not permit concurrent access—if it did, we could use a single delegate
throughout).
We then call BeginInvoke. This begins execution while
immediately returning control to the caller. In accordance with our delegate,
we must pass a string to BeginInvoke (the compiler enforces this, by manufacturing
typed BeginInvoke and EndInvoke methods on the delegate type).
BeginInvoke requires two further arguments—an optional
callback and data object; these can be left null as they're usually not
required. BeginInvoke returns an IASynchResult object which acts as a
cookie for calling EndInvoke. The IASynchResult object also has the property
IsCompleted which can be used to check on progress.
We then call EndInvoke on the delegates, as their results
are needed. EndInvoke waits, if necessary, until its method finishes,
then returns the method's return value as specified in the delegate (string,
in this case). A nice feature of EndInvoke is that if the DownloadString
method had any ref or out parameters, these would be added into EndInvoke's
signature, allowing multiple values to be sent back by to the caller.
If at any point during an asynchronous method's execution
an unhandled exception is encountered, it's re-thrown on the caller's
thread upon calling EndInvoke. This provides a tidy mechanism for marshaling
exceptions back to the caller.
If the method you're calling asynchronously has no return
value, you are still (technically) obliged to call EndInvoke. In a practical
sense this is open to interpretation; the MSDN is contradictory on this
issue. If you choose not to call EndInvoke, however, you'll need to consider
exception handling on the worker method.
Asynchronous Methods
Some types in the .NET Framework offer asynchronous versions
of their methods, with names starting with "Begin" and "End".
These are called asynchronous methods and have signatures similar to those
of asynchronous delegates, but exist to solve a much harder problem: to
allow more concurrent activities than you have threads. A web or TCP sockets
server, for instance, can process several hundred concurrent requests
on just a handful of pooled threads if written using NetworkStream.BeginRead
and NetworkStream.BeginWrite.
Unless you're writing a specialized high concurrency
application, however, you should avoid asynchronous methods for a number
of reasons:
- Unlike asynchronous delegates, asynchronous methods may not actually
execute in parallel with the caller
- The benefits of asynchronous methods erodes or disappears if you
fail to follow the pattern meticulously
- Things can get complex pretty quickly when you do follow the pattern
correctly
If you're simply after parallel execution, you're better
off calling the synchronous version of the method (e.g. NetworkStream.Read)
via an asynchronous delegate. Another option is to use ThreadPool.QueueUserWorkItem
or BackgroundWorker—or simply create a new thread.
The WebClient class
actually offers a built-in method called DownloadStringAsync which
provides asynchronous-like functionality. For now, we'll ignore
this and focus on the mechanism by which any method can be called
asynchronously. |
Asynchronous Events
Another pattern exists whereby types can provide asynchronous
versions of their methods. This is called the "event-based asynchronous
pattern" and is distinguished by a method whose name ends with "Async",
and a corresponding event whose name ends in "Completed". The
WebClient class employs this pattern in its DownloadStringAsync method.
To use it, you first handle the "Completed" event (e.g. DownloadStringCompleted)
and then call the "Async" method (e.g. DownloadStringAsync).
When the method finishes, it calls your event handler. Unfortunately,
WebClient's implementation is flawed: methods such as DownloadStringAsync
block the caller for a portion of the download time.
The event-based pattern also offers events for progress
reporting and cancellation, designed to be friendly with Windows applications
that update forms and controls. If you need these features in a type that
doesn't support the event-based asynchronous model (or doesn't support
it correctly!) you don't have to take on the burden of implementing the
pattern yourself, however (and you wouldn't want to!) All of this can
be achieved more simply with the BackgroundWorker helper class.
(Sưu tầm) |