Multithreaded programming includes the following complexities.
To deal with this complexity, C# includes the asynchronous results pattern. This section demonstrates how to use the asynchronous results pattern and shows how it simplifies at least the first three complexities associated with multithreading.
Introducing the Asynchronous Results Pattern
With the asynchronous results pattern, you do not code using the Thread class explicitly. Instead, you use delegate instances. Consider the code in Listing 16.1.
Listing 16.1. Asynchronous Results Pattern Example
The results of Listing 16.1 appear in Output 16.1.
Main() begins by instantiating a delegate of type WorkerThreadHandler. As part of the instantiation, the DoWork() method is specified as the method to execute on a different thread. This line is similar to the instantiation of ThreadStart in Chapter 15, except you use your own delegate type, WorkerThreadHandler, rather than one built into the framework. As you shall see shortly, this allows you to pass custom parameters to the thread.
Next, the code calls BeginInvoke(). This method will start the DoWork() method on a thread from the thread pool and then return immediately. This allows you to run other code asynchronously with the DoWork() method. In this example, you print periods while waiting for the DoWork() method to complete.
You monitor the DoWork() method state through a call to IAsyncResult.AsyncWaitHandle.WaitOne() on asyncResult. Like AutoResetEvent.WaitOne(), IAsyncResult.AsyncWaitHandle.WaitOne() will return false if the timeout (1000 milliseconds) expires before the thread ends. As a result, the code prints periods to the screen each second during which the DoWork() method is executing. In this example, the mock work in DoWork() is to pause for one second.
Finally, the code calls EndInvoke(). It is important to pass to EndInvoke() the IAsyncResult reference returned when calling BeginInvoke(). IAsyncResult contains the data about the executing worker thread. If the thread identified by the IAsyncResult parameter is still executing, then EndInvoke() will block and wait for the DoWork() method to complete. EndInvoke() does not abort a thread, but blocks the thread until it is done. In this example, there is no blocking because you poll the thread' state in the while loop and call EndInvoke() only after the thread has completed.
Passing Data to and from an Alternate Thread
The introductory example in Listing 16.1 didn't pass any data to or receive any data back from the alternate thread. This is rather limiting. Passing data using fields is an option, but in addition to being cumbersome, such a solution requires the programmer of the called method to explicitly code for an asynchronous call, rather than just an arbitrary method that the caller wants to invoke asynchronously. In other words, the called method must explicitly access its required data from the member fields instead of having the data passed in via a parameter. Fortunately, the asynchronous results pattern handles this explicitly.
To begin, you need to change the delegate data type to match the signature of the method you are calling asynchronously. Consider, for example, a method called GetFiles() that returns an array of filenames that match a particular search pattern. Listing 16.2 shows such a method.
Listing 16.2. Target Method Sample for an Asynchronous Invocation
As input parameters, this method takes the search pattern and a bool indicating whether to search subdirectories. It returns an array of strings. Since the method could potentially take some time to execute, you decide (perhaps after implementing the method) to call it asynchronously.
In order to call GetFiles() asynchronously using the asynchronous results pattern, you need a delegate to match the method signature. Listing 16.3 declares such a delegate instead of relying on an existing delegate type.
Listing 16.3. Asynchronous Results with Completed Notification
Given the delegate, you can declare a delegate instance and call BeginInvoke(). Notice that the signature for BeginInvoke() is different from the signature in the asynchronous results pattern in Listing 16.1. Now there are four parameters. The last two correspond to the callback and state parameters. These parameters were in the BeginInvoke() call of Listing 16.1 and will be investigated shortly. There are two new parameters at the beginning, however, and this is not because BeginInvoke() is overloaded. The new parameters are searchPattern and recurseSubdirectories, which correspond to the GetFilesHandler delegate. This enables a call to GetFiles() using the asynchronous results pattern while passing in the data that GetFiles() needs (see Listing 16.4).
Listing 16.4. Passing Data Back and Forth Using the Asynchronous Results Pattern
The results of Listing 16.4 appear in Output 16.2.
As demonstrated in Listing 16.4, you also need to retrieve the data returned from GetFiles(). The return from GetFiles() is retrieved in the call to EndInvoke(). Just as the BeginInvoke() method signature was generated by the compiler to match all input parameters on the delegate, the EndInvoke() signature matches all the output parameters. The return from getFilesMethod.EndInvoke(), therefore, is a string, matching the return of GetFiles(). In addition to the return, any parameters marked as ref or out would be part of the EndInvoke() signature as well.
Consider a delegate type that includes out and ref parameters, as shown in Figure 16.1.
Figure 16.1. Delegate Parameter Distribution to BeginInvoke() and EndInvoke()
An in parameter, such as data, only appears in the BeginInvoke() call because only the caller needs to pass such parameters. Similarly, an out parameter, changeDescription, only appears in the EndInvoke() signature. Notice, however, that the ref parameter (value) appears in both BeginInvoke() and EndInvoke(), since a ref parameter is passed into both the function and a parameter via which data will be returned.
In summary, all delegates created by the C# compiler include the BeginInvoke() and EndInvoke() methods, and these are generated based on the delegate parameters.
Receiving Notification of Thread Completion
Listing 16.1 and Listing 16.4 poll to determine whether the DoWork() or GetFiles() method is running. Since polling is generally not very efficient or convenient, a notification mechanism that fires an event once the thread has completed is preferable. This is what the AsyncCallback delegate type is for, and an instance of this delegate is passed as the second-to-last parameter of BeginInvoke(). Given an AsyncCallback instance, the async pattern will invoke the callback delegate once the method has completed. Listing 16.5 provides an example, and Output 16.3 shows the results.
Listing 16.5. Asynchronous Results with Completed Notification
Callback notification when the worker thread completes provides a key benefit of using the asynchronous results pattern over manual thread manipulation. For example, it allows developers to display a widget to indicate that a task has completed.
As already demonstrated, EndInvoke() can be called from within Main() using the delegate instance and the IAsyncResult reference returned from BeginInvoke(). However, EndInvoke() will block until the asynchronous call completes. As a result, it is preferable to call EndInvoke() from within the callback. To accomplish this, the callback function casts its IAsyncResult parameter, but the cast is unintuitive. The data type is AsyncResult, but the namespace is the unintuitive System.Runtime.Remoting.Messaging. SearchCompleted() demonstrates the code for calling EndInvoke() from within the thread completion callback. Listing 16.6 shows the fully qualified call.
Listing 16.6. Calling EndInvoke() from within Asynchronous Callback
Passing Arbitrary State
The last parameter in the BeginInvoke() call is of type object, and it provides a mechanism for passing arbitrary data to the callback method (SearchCompleted()). Consider a situation in which multiple threads were started one after the other, and in each case the callback was to the same AsyncCallback delegate instance. The problem is that from within the async callback delegate you don't correlate which completed call to GetFiles() corresponds to which call that initiated the GetFiles() method. For example, you cannot print out which search results correspond to which searchPattern.
Fortunately, this is the purpose of the last parameter in BeginInvoke(). Listing 16.7 starts the GetFiles() method for each search pattern passed in the command line. In each call, you pass the search pattern (arg) twice to asyncGetFilesHandler.BeginInvoke(): once as a parameter to the GetFiles() method that is to run asynchronously, and once as the last parameter to be accessed from inside SearchCompleted().
Listing 16.7. Asynchronous Results with Completed Notification
The results of Listing 16.7 appear in Output 16.4.
Since the listing passes arg (the search pattern) in the call to BeginInvoke(), it can retrieve the search pattern from the IAsyncResult parameter of SearchCompleted(). To do this, it accesses the IAsyncResult.AsyncState property. Because it is of type object, the string data type passed during BeginInvoke() needs to be downcast to string. In this way, it can display the search pattern above the list of files that meet the search pattern.
Asynchronous Results Conclusions
One of the key features that the asynchronous results pattern offers is that the caller determines whether to call a method asynchronously. The called object may choose to provide asynchronous methods explicitly, perhaps even using the asynchronous results pattern internally. However, this is not a requirement for the caller to make an asynchronous call.
Called methods may choose to provide asynchronous APIs explicitly when the called class can implement the asynchronous functionality more efficiently than the caller can, or when it is determined that asynchronous method calls are likely, such as with methods that are relatively slow. If it is determined that a method should explicitly provide an asynchronous calling pattern, it is a good practice for the API to follow the asynchronous results design pattern. An explicit implementation, therefore, should include methods that correspond to BeginInvoke(), along with events that callers can subscribe to in order to be notified when a thread completes.
For example, in .NET 2.0, the System.Net.WebClient class includes asynchronous method calls for downloading files. To begin the download, it includes the DownloadFileAsync() method. Additionally, the caller can register for notification when the download is complete using the DownloadFileCompleted event.
The .NET 2.0 implementation of System.Net.WebClient also includes a DownloadProgressChanged event for publishing when a download operation successfully transfers some of its data, as well as a CancelAsync() method to discontinue the download. These are not explicitly part of the asynchronous results pattern, and therefore, they are not available unless provided by the called class explicitly. These methods are another reason why programmers may explicitly implement asynchronous APIs, instead of just relying on the caller to use the asynchronous results pattern. To help with such methods, .NET 2.0 includes the System.ComponentModel.BackgroundWorker class.
Although not supplied by System.Net.WebClient, a method corresponding to EndInvoke() is also a nice addition for explicitly implemented asynchronous calls.
The asynchronous results pattern relies on the built-in thread pool that provides support for reusing threads rather than always creating new ones. The pool has a number of threads, dependent on the number of processors, and the thread pool will wait for a free thread before servicing the request. If a request for a new thread is needed but the creation of a new thread would undermine the value of multithreading (because the cost of an additional thread would outweigh its benefit), the thread pool won't allocate a thread until another thread is released back to the thread pool.