|
We’ve seen how events use delegates internally. The event mechanism that I’ve described covers most situations, but occasionally we’ll need to do something different. For example, we might not want our use of callbacks exposed in the public event list, preferring it to be private for those in the know. Or we might want to perform some business logic when a listener connects a handler to our delegate, perhaps checking to see whether the listener’s developer license for our component is current, in which case we’d need to replace the automatic event hookup discussed previously. In such cases, we’ll want to use delegates at a lower level of abstraction, closer to their core. I’ve written a sample program, shown in Figure 8-7, that demonstrates this process by using delegates to illustrate the asynchronous operation of function calls.
A low-level delegate example begins here, demonstrating asynchronous operation.
Figure 8-7: Sample program using delegates for asynchronous operation.
When a managed program makes a function call, it waits for the call to return before going on to the next line of code. This functionality at first makes it relatively easy to write consistent programs—do this, then do that with the result of this, then do something else with the result of that, and so on until you’re finished or you get sick of it. But while your thread is waiting for the function to return, it isn’t servicing its internal hidden message loop that handles user interface events such as mouse clicks and keystrokes. Your application’s user interface is frozen for the length of the function call. Most function calls are short enough that it doesn’t matter, but you can’t leave the user interface frozen for more than a couple of seconds or the user will get really annoyed. If your function call takes longer than this, say in recalculating a spreadsheet, you have to write code that will hand the function off to some sort of background processing, continue to service the user interface while the background operation takes place, and harvest the results at some later time. You could write multithreaded code, as described in the next chapter, but this approach can become complicated and is often overkill for a simple user interface application. We’d like a simple, easy to use, hard to break way of performing function calls asynchronously.
Objects that expose methods that take a long time to finish processing often provide assistance by splitting their functionality. They’ll expose a method named BeginSomething that starts an operation in the background and returns quickly. They’ll also expose a corresponding EndSomething method to harvest the result, with some other method to check whether the result is ready. But for various reasons, not every object is written that way. Maybe an object is slow only in certain situations such as encountering large data sets, or maybe the developer has a superfast machine and doesn’t realize that many of his less fortunate customers are still running the creaky, ancient machines they bought nine months ago. We’d like our clients to be able to invoke any object method asynchronously, regardless of whether the object’s developer has taken the time to write code that handles this case.
We need a way of calling object methods asynchronously.
Delegates allow us to do this. Remember, a .NET delegate is an object that wraps a pointer to another object method, the target function. The delegate contains logic for invoking the target function. One of these pieces of invocation logic is a standardized way of performing a call to the target function asynchronously. The client starts the operation by calling a method on the delegate called BeginInvoke. This call hands off the function call to a different thread of execution (selected from a pool that the system creates for just such a purpose, as discussed in the next chapter) and returns quickly, so your user interface thread is free to perform its other tasks. The background thread calls the target function and waits for the result, which it returns to the delegate. At some later time, the client calls the delegate’s EndInvoke method to harvest the result of the asynchronous function call. The process is shown in Figure 8-8.
.NET delegates support asynchronous operation of any function call.
Figure 8-8: Asynchronous operation.
The sample client program creates an object (OrdinaryObject) and calls its GetTime method. I’ve written the method to wait for 5 seconds before returning the time. Rather than hang the user interface while waiting for the method to complete (try it and see; you’ll be surprised how annoying even this silly example can be), I want to make this call asynchronously, which means that I need a delegate that wraps the target method. The code for creating the delegate is shown in Listing 8-8. First I declare my delegate type, which is conceptually similar to deriving a new class from System.Delegate except that you must use the features built into your compiler to do it rather than the usual inheritance notation. I declare a delegate with the signature of the target function I want it to wrap. I then create an object of the class that I want to use the delegate to call. The object doesn’t need to know anything about delegates and asynchronous operation, and you can see from the source code that this one doesn’t. Finally I create an instance of my new delegate class, passing it the target function pointer in its constructor. The delegate is now wrapping the target function on this particular object instance. I can now call the target function synchronously through the delegate, using the Invoke method in Visual Basic .NET or the target method name (in this case GetTime) in C#.
Listing 8-8: Declaring and creating the delegate for asynchronous operation.
Public Class Form1 Inherits System.Windows.Forms.Form ’ Using the compiler’s keyword, declare a delegate class with the ’ signature of the target function I want it to call. Delegate Function GetTimeDelegate(ByVal ShowSeconds As Boolean) _ As String ’ Create an ordinary object that doesn’t know anything about delegates ’ or asynchronous operation Dim OrdinaryObject As New AsyncFuncCallComponentVB.Class1() ’ Create a new instance of the delegate class, wrapping the target ’ function I want to use the delegate to invoke. Dim DelegateInstance As New GetTimeDelegate( _ AddressOf OrdinaryObject.GetTime) < handler functions omitted, see next figure> End Class
A delegate contains methods named BeginInvoke and EndInvoke to perform function calls asynchronously. You won’t see these methods in the documentation of the Delegate class, as they are not members of the underlying class but are instead generated by the compiler. The reason for this is that BeginInvoke accepts all the parameters of the target function, in addition to several of its own, which means that the parameter list will be different from one delegate to another. This is a good way of handling delegates, and it works about the way you think it should, but I still think this functionality could have been better documented. IntelliSense will show the BeginInvoke and EndInvoke methods in both Visual Basic and C#. In case you haven’t figured it out by now, BeginInvoke starts the asynchronous function call and returns quickly, while EndInvoke harvests the results at completion.
A delegate contains the methods BeginInvoke and EndInvoke to support asynchronous operation.
In order to invoke my method asynchronously, I first need to decide how I want to find out when the operation completes. The two choices are polling or a callback function. I’ll discuss the former case first, using the code shown in Listing 8-9.
Listing 8-9: Functions for an asynchronous call with a polled completion signal.
Dim result As System.IAsyncResult ‘ User clicked button to begin asynchronous function call. ‘ Call BeginInvoke and store IAsyncResult that we get ‘ in return Private Sub Button3_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button3.Click result = DelegateInstance.BeginInvoke(CheckBox1.Checked, _ Nothing, Nothing) End Sub ‘ User clicked button to check for completion of asynchronous call. ‘ Check the IsCompleted property and report value. Private Sub Button4_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button4.Click MessageBox.Show(result.IsCompleted.ToString) End Sub ‘ User clicked button to harvest result of asynchronous call. ‘ Call EndInvoke, passing IAsyncResult, to get return value ‘ of called function. Private Sub Button5_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button5.Click Label1.Text = DelegateInstance.EndInvoke(result) End Sub
When I call BeginInvoke, it starts the asynchronous function call as discussed previously and returns an object of type IAsyncResult. This call represents my connection to the asynchronous operation, sort of like the order number you get when you buy books from Amazon.com. To find out whether the operation is complete, I read the IsCompleted property from this object. The value of IsCompleted will be false until the operation is done, at which time the value becomes true. When I’m ready to use the result, I call EndInvoke, passing the IAsyncResult object, and it returns the return value of the target function. If I call EndInvoke before the target function has returned, EndInvoke will block until the operation completes.
Like most programmers, I don’t care for polling. You waste a lot of time saying, “Are you done yet?” and the polled object wastes a lot more saying, “No, I’m not. Quit wasting all my CPU cycles and maybe I’ll get something done, OK?” We’d rather be notified of the completion by some sort of callback. Since this chapter is about delegates, it shouldn’t surprise you too much to learn that we do this callback notification by passing a delegate. The code for this case is shown in Listing 8-10.
There are several ways to wait for the operation to complete.
Listing 8-10: A handler function for an asynchronous method invocation with a callback completion signal.
‘ User clicked button to begin invocation of asynchronous ‘ function call, passing callback delegate to receive the ‘ signal of completion. Private Sub Button6_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button6.Click DelegateInstance.BeginInvoke(CheckBox1.Checked, _ AddressOf MyOwnCallback, Nothing) End Sub ‘ Callback function that receives notification of asynchronous ‘ completion. Harvest result by calling EndInvoke. Public Sub MyOwnCallback(ByVal ar As IAsyncResult) Label1.Text = DelegateInstance.EndInvoke(ar) End Sub
When we call BeginInvoke on the delegate wrapping our object’s target function, we pass as its second parameter (in this example, but the position will vary because BeginInvoke wraps target functions with different numbers of parameters) another delegate, this one of type System.AsyncCallback. This delegate wraps a target function that we have written to receive the notification of the function completion. When the operation completes, the .NET Framework uses the delegate to call my target function. It passes me an IAsyncResult object, which I use to call EndInvoke and retrieve the return value.
Or I can make it call me back by using another delegate.
My customers report that asynchronous invocation backfires on them every once in a while if they aren’t paying close attention. There’s no problem calling the function or harvesting the result. The problem arises if the calling program modifies or locks some code or data that the asynchronous function needs while the call is in progress. Using asynchronous invocation puts you into a multithreaded situation in a way that isn’t immediately apparent (until someone points it out; then you go, “Doh! [slap]”). If you plan on using it, make sure you understand multithreaded operations, as the next chapter discusses. (How’s that for a segue?)
|