Causally Connectable
Calling Web Services Asynchronously
October 30, 2009
VBUG Spotlight
LANGUAGES: C# |VB.NET
ASP.NET VERSIONS:1.1
Causally Connectable
Calling Web Services Asynchronously
By Rob MacDonald
We ve all bought into the Web services message by now, andfully understand the place that Web services can have in distributed systemdesigns. We ve also learned that interacting with Web services is best done ina chunky rather than chatty way. One reason for this is that it is alwaysbest to assume there is some latency when talking to Web services there salways the chance that it will take some time to respond. Even if the latencyis only a second or so, a really solid user interface shouldn t hang waitingfor its response. It should allow the user to carry on interacting with theapplication.
In this article, I ll present some of the techniques thatare available for calling Web services asynchronously. For broad coverage, I lluse .NET 1.1 for all my examples. .NET 2.0 makes things slightly easier, butthe same principles apply.
Reality or Perception?
When we talk to Web services, we communicate over HTTP,which is a synchronous (request/response) protocol. So how do we make it appearto be asynchronous? The easiest way is to make sure the call to the Web serviceis performed on a background thread in our client. That way, the Web serviceneeds no modification, and in the client only a background thread is keptwaiting the main thread serving the user interface carries on doing what itis supposed to do, giving the perception that the Web service is being calledasynchronously.
Don t worry if, like many programmers, you re not toofamiliar with multi-threaded programming; with some help from .NET and a littlebit of discipline, you can avoid all the complex threading issues.
Getting Started
I m going to focus on how Windows Forms programmers canwork with a Web service asynchronously. To do this, I need to set up a Web service(all code examples are available for download in VB.NET and C#; see end ofarticle for details). I have created a Web service named Slow with a single Webmethod, coded as follows:
_
Public Function HelloWorld() As String
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(5))
Return "Hello Worldat " & DateTime.Now.ToString()
End Function
This code is enough to simulate a long-running Web methodthat takes five seconds to return. From now on, everything I do will be on theclient side.
To help demonstrate the options available, I ve written aWindows Form application that calls the Slow Web service s HelloWorld methodfour different ways. You can see what this looks like in Figure 1. Note thatthe application has four sections; each one calls the Web service using adifferent technique. Note also the bright red bar at the bottom. When theapplication s user interface is responsive (that is, it isn t hanging whileawaiting a response from the Web service), this bar flashes red and blue atregular intervals. Consider it as a heartbeat all is well while the heart isbeating; when it s locked, so too is the application.
Figure 1: The AsyncTest Web service client.
The Synchronous Call
Let s look first at the code for the synchronous call.Behind the synchronous call s Call the Server button is the following code:
'pxy = New Slow.Service1
SyncCallThreadLabel.Text = getThreadType()
Dim s As String = pxy.HelloWorld()
SyncResponseThreadLabel.Text = getThreadType()
SyncResponseLabel.Text = s
No surprises here. The proxy variable (pxy) is actuallyinitialised to a Web reference object in the application s Form_Load event handler,so here the code simply calls HelloWorld and assigns the response to s before displaying it in a Label control. This is easy enough; the problem isthat the entire user interface (including the heartbeat) freezes for fiveseconds as soon as HelloWorld is called.
Before seeing how to remedy this situation, it s worthtaking a look at the code in the getThreadType method, which you can see iscalled twice in the previous code. Here it is:
' returns whether the current thread is a UI or a worker
' (background) thread
Private Function getThreadType() As String
Dim currentThreadID AsString = _
AppDomain.GetCurrentThreadId().ToString()
IfThread.CurrentThread.IsBackground Then
Return "Background(" + currentThreadID + ")"
Else
Return "UI ("+ currentThreadID + ")"
End If
End Function 'getThreadType
This code returns a string containing the Thread ID of thecurrently executing thread, and also whether this thread is a background or UIthread. You ll see why this matters later.
You can see the result of executing this code in Figure 2.Note that both the call and the response are handled on the same thread.
Figure 2: The result of a synchronouscall.
The Asynchronous Call (Blocking)
Let s now look at blocking, the first of three differentways of calling the Web service asynchronously (each approach has its strengthsand weaknesses).
In the blocking approach, I make the call to the Web serviceasynchronously and then carry on doing some useful work. When I ve finisheddoing the useful work, I then block until the Web service returns. Here s thecode (slightly simplified):
BlockingCallThreadLabel.Text = getThreadType()
Dim ar As IAsyncResult = _
pxy.BeginHelloWorld(Nothing, Nothing)
doYieldingWork(blockTime)
Dim s As String = pxy.EndHelloWorld(ar)
BlockingResponseThreadLabel.Text = getThreadType()
BlockingResponseLabel.Text = s
The first thing to note is that I m not calling HelloWord!Whenever you create a Web Reference, the proxy code that is generated for youcontains three methods for each Web method in the Web service. One of these isthe method we use for synchronous calls; the other two (prefixed Begin and End)are for asynchronous calls.
You will see that BeginHelloWorld takes two arguments.These can be used to help me control the asynchronous operation; I ll explainthem later. In this example, I ve passed Nothing forboth of these arguments. (If the HelloWorld method took arguments, they wouldappear in BeginHelloWorld directly after these two control arguments.) You willalso see that BeginHelloWorld doesn t return a string. Instead, it returns anobject that implements the IAsyncResult interface, which I ve assigned to avariable named ar.
When you call BeginHelloWord, .NET immediately takes abackground thread from its thread pool and executes the real call to HelloWorldon this thread. You don t need to worry about the thread management. It thenimmediately returns the IAsyncResult object so that your code can carry onwithout waiting for the Web service.
In my example, I have a method called doYieldingWork,which actually doesn t do anything interesting (but the point is, it could). Itcarries on doing uninteresting things for blockTime number of seconds (which iscontrollable from my user interface, and defaults for two seconds).
When I have finished doing other things and am ready formy Web service results, I call EndHelloWorld, passing it the ar variable thatidentifies which set of results I want. If the results are already available,EndHelloWorld simply returns them. If not, it blocks, waiting for thebackground thread to receive the return value from the Web service.EndHelloWorld returns exactly the same type as HelloWorld (in this case, astring). I handle the return from EndHelloWorld in exactly the same way as Idid the synchronous result. Figure 3 shows my user interface after the call hasreturned.
Figure 3: The result of a blocking call.
Just as for the synchronous call, all my code has run onthe main user interface thread. This is handy, as I don t have to worry aboutthread management or data marshalling. However, the blocking technique isn tthat much better than the synchronous technique. It can be useful if you wantto call a Web service at the beginning of a complex task, because you can carryon with the rest of the task while waiting for the Web service to return butit can still involve locking the UI.
The Asynchronous Call (Polling)
Polling provides a more flexible async technique thanblocking. With polling, you make the call on a background thread, then checkfrom time to time to see if the results are ready. My polling code is verysimilar to the blocking code:
PollingCallThreadLabel.Text = getThreadType()
Dim ar As IAsyncResult = _
pxy.BeginHelloWorld(Nothing, Nothing)
While Not ar.IsCompleted
doYieldingWork(pollInterval)
End While
Dim s As String = pxy.EndHelloWorld(ar)
PollingResponseThreadLabel.Text = getThreadType()
PollingResponseLabel.Text = s
I ve used BeginHelloWorld and EndHelloWorld in much thesame way as before. The difference is that I now have a loop. In the loop, Icheck the IsCompleted property on the ar variable. If IsCompleted returnsfalse, I carry on doing other things; but if it returns true, I can retrievethe results using EndHelloWorld knowing that I will never be blocked, becausethe async operation has completed. Figure 4 shows my user interface for usingthe polling technique. Note that, again, the call and reply are handled on thesame thread.
Figure 4: Using the pollingtechnique.
Polling is a much more useful approach than blocking inmost circumstances. Rather than using a loop, as I have here, a more generalapproach uses a Timer control firing every second or two, checking for returnsfrom all your Web service calls. You can design a polling approach that willhandle all your Web service requests asynchronously, without ever locking theuser interface. In fact, many applications work exactly this way. The mainproblem is one of control. If you only call one or two Web methods, polling isfine. But for more complex requirements, it can require some effort to handleall the different responses through a Timer control.
The Asynchronous Call (Callback)
In many ways, the most convenient async approach is to usecallbacks. As you can see in the following code, it looks simple enough andin many ways, it is but it does create one problem that you need to resolvewith considerable care. Here s the code behind the callback Call the Serverbutton:
' define an ansync callback delegate that will call
' callbackFunction
Dim cb As New AsyncCallback(AddressOf callbackFunction)
CallbackCallThreadLabel.Text = getThreadType()
pxy.BeginHelloWorld(cb, Nothing)
Start by looking at the last line of code. I m callingBeginHelloWorld again, but this time I am supplying a value for the firstargument. This argument is a delegate (basically, a managed pointer to amethod) of type AsyncCallback, which is pointing to a method calledcallbackFunction. There s nothing special about callbackFunction, it s simplythe name of a method I ve written (any name will do). Here s its definition:
Private Sub callbackFunction(ByVal ar As IAsyncResult)
Dim s As String =pxy.EndHelloWorld(ar)
CallbackResponseThreadLabel.Text = getThreadType()
CallbackResponseLabel.Text = s
End Sub 'callbackFunction
The callbackFunction must take an argument of type IAsyncResult,which you saw earlier. .NET takes care of calling callbackFunction, as well aspassing it an object that implements IAsyncResult, when the Web servicereturns. Using IAsyncResult, I can call EndHelloWorld and retrieve its returnvalue, as you can see. So the callback technique does look convenient: I callBeginHelloWorld and pass it a reference to the code I would like it to callwhen the Web service returns. In the meantime, my code running on the main userinterface thread can carry on. No blocking or frozen UI, and no concerns overcomplex control code all I need to do is handle the response in a differentmethod from the one that makes it (callbackFunction is basically an eventhandler). Take a look at Figure 5 though, and you will see there s a little bitmore to it than that.
Figure 5: Using a callback.
Notice that the reply thread is different than the callthread. All the code in callbackFunction executes not on the main UI thread,but on a background thread. This has not caused a problem for my demonstrationcode, but only because I ve been lucky. There are in fact two issues you needto address when running on a background thread in a Windows Forms application.The first issue is one of contention. All kinds of horrible things can happenif code on two different threads accesses the same data at the same time.Consequently, you either need to ensure that code running on two differentthreads can t access the same variables, or you need to write synchronisationcode around every access to every bit of data that may be shared between twothreads. .NET provides great features for writing this kind of code but ifyou ve not done it before, be afraid (be very afraid).
The second issue is specific to Windows Forms and the waythat Windows handles its user interfaces. Put simply, Windows is (currently)designed such that all user interface features (forms, controls, etc.) in agiven program always run on a single thread specially allocated for UIprocessing. It is simply not safe to access your UI from any thread other thanyour program s UI thread. You will see what I mean if you download the demonstrationcode and try out the two TreeView buttons. One button attempts to update aTreeView control from the UI thread; the other tries to do the same from abackground thread. Both buttons call exactly the same code:
Private Sub AddToTreeView()
DemoTreeView.Nodes.Add(DateTime.Now.ToString())
End Sub 'AddToTreeView
It looks harmless enough, but it fails when called fromthe background thread. This isn t something particular to TreeView controls: itapplies to all UI operations attempted from a background thread. You re notsupposed to go there (remember, I was just lucky with the callback code thatupdates a Label control).
Fortunately, there is a solution to this problem thatavoids the need to address complex threading issues. Accessing UI controls froma background thread is a common problem, so Windows Forms provides an easy wayof making it safe. Every control in Windows Forms supports a method calledInvoke. This method allows a background thread to ask the main UI thread toinvoke a method and run that method on the UI thread. You can pass arguments tothe method, which allows you to safely marshal data from the background threadto the UI thread without worrying about writing synchronisation code aroundshared data.
So, let me now re-write my callback code to work safely.Here goes:
Private Delegate Sub UpdateDelegate(ByVal s As String)
Private Sub callbackFunction(ByVal ar As IAsyncResult)
Dim s As String =pxy.EndHelloWorld(ar)
Me.Invoke( _
NewUpdateDelegate(AddressOf UpdateUISafely), _
New Object() {s})
End Sub 'callbackFunction
Private Sub UpdateUISafely(ByVal s As String)
CallbackResponseThreadLabel.Text = getThreadType()
CallbackResponseLabel.Text = s
End Sub
In this code, I ve declared my own delegate(UpdateDelegate) that can point to any method that takes a single string as anargument. Now if you look at the new callbackFunction, you ll see that I callInvoke, passing it an instance of UpdateDelegate pointing at a method namedUpdateUISafely. If the method you want to invoke takes arguments (which minedoes), these are supplied in the form of an array as the second argument toInvoke. So now my callback function, which runs on a background thread, usesInvoke to get the UI code in UpdateUISafely executed on the main UI thread.Take a look at Figure 6 and you will see that my user interface is now beingupdated from the UI thread rather than a background thread.
Figure 6: Callback invoking code onthe UI thread.
By using Invoke, I can overcome the difficulties thatarise because the callback function executes on a background thread. Of course,there s no point calling Invoke from the main UI thread; it works, but involvesmore code and the overhead that .NET incurs doing the synchronisation and datamarshalling for you. Although you won t see it in VB.NET s IntelliSense, everycontrol implements a property called InvokeRequired that returns true if youcall it from a background thread.
I ve left the lucky code in the download, so you can seethat the callback gets handled on a background thread. But you ll see that I vecommented out the safe code, so it will be easy to try it both ways.
Conclusion
You can make your client applications into smart clientapplications by handling all your calls to Web services asynchronously.Although your program still makes a synchronous call to the Web service, thisis handled on a background thread in your client, so your UI is always incontrol. You simply need to handle the synchronisation between the userinterface thread and the background thread when the results become available.
In this article I ve looked at three different ways ofdoing this: blocking, polling, and callbacks, and shown that, although thecallback function you supply when using this third technique runs on abackground thread, you can safely use the Control.Invoke technique to marshalthe callback onto the UI thread, thus avoiding any further threading issues.
If you re ready to start working with Windows Forms 2.0,take a look at the BackgroundWorker control. This control makes it easy for youto separate out your UI and background thread code, and handles the marshallingfor you, removing the need to use a delegate and call Invoke. Otherwise, itdoes much the same as we have done here.
Whichever approach you take, there is more and moreemphasis on asynchronous communications these days, and this is a trend that sgoing to run. Now is a great time to start getting familiar with asynctechniques.
The sample code inthis article is available for download.
VBUG member Rob MacDonaldhas more than 20 years of programming experience, mostly with Microsofttechnologies. He has worked exclusively with .NET for more than five years, andhas spent much of that time creating learning material for Microsoft, frominternal Microsoft technical courses covering topics including ADO.NET, Web services,and architecture for the benefit of Microsoft consultants and engineers, to thewidely distributed Learn247 DVD Series. Prior to that, Rob authored severalbooks and earned his spurs as project manager and lead programmer on someserious projects. When not doing IT, Rob is the UK sleading commercial stoat breeder.
About the Author
You May Also Like