Custom Threaded Objects

Technically, there is no relation between threads and objects. A single method in an object can be executed by several different threads, and a single thread can span multiple methods in different objects. However, it's often useful to create a dedicated class that encapsulates everything a thread needs. This provides an easy way to associate pieces of required information with a thread and overcome the fundamental limitation that a thread can execute only methods that take no parameters.

Consider, for example, the asynchronous calls to an imaginary stock quote XML Web service, which we considered earlier in the chapter. The GetStockQuote Web method required a single ticker parameter and returned a decimal with price information. You can easily create a class that encapsulates this information (as shown in Listing 6-21).

Listing 6-21 Encapsulating the details of a method call
 Public Class StockQuoteInfo     Public TickerParameter As String     Public PriceReturnValue As Decimal     Public Sub New(ticker As String)         TickerParameter = ticker     End Sub     Public Sub New()         ' This is the default constructor.     End Sub End Class 

Now it's just a small leap to incorporate this into a custom threading class that encapsulates the method parameters and the code required to call the stock lookup methods, as shown in Listing 6-22.

Listing 6-22 Creating a custom class for an asynchronous method call
 Public Class StockQuoteLookup     Public StockCall As New StockQuoteInfo()     Private Proxy As New StockQuoteService()     Public Sub DoLookup()         StockCall.PriceReturnValue = Proxy.GetStockQuote( _          StockCall.TickerParameter)     End Sub End Class 

Note that we use the synchronous GetStockQuote method from the proxy class, not the asynchronous BeginGetStockQuote. That's because the entire DoLookup method will execute asynchronously from the rest of the application. Essentially, this moves the responsibility for the asynchronous call from the proxy class to our client code (as shown in Figure 6-6).

Figure 6-6. Using a custom threaded class

graphics/f06dp06.jpg

You can easily use the StockQuoteLookup class synchronously, as shown in Listing 6-23.

Listing 6-23 Calling an XML Web service synchronously by using a custom class
 Dim Lookup As New StockQuoteLookup() Lookup.StockCall.Ticker = "MSFT" Lookup.DoLookup() MessageBox.Show("MSFT is at: " & _  Lookup.StockCall.PriceReturnValue.ToString()) 

You can also use the StockQuoteLookup class asynchronously by creating and using a Thread object. Listing 6-24 shows an example that uses this technique and then polls the class to determine when processing is complete (at which point it is safe to read the return value).

Listing 6-24 Calling an XML Web service asynchronously by using a custom class
 Dim Lookup As New StockQuoteLookup() Lookup.StockCall.Ticker = "MSFT" Dim LookupThread As New Thread(AddressOf Lookup.DoLookup) LookupThread.Start() Do Until (LookupThread.ThreadState And ThreadState.Stopped)     ' (Do other tasks if desired) Loop MessageBox.Show("MSFT is at: " & _  Lookup.StockCall.PriceReturnValue.ToString()) 

One of the advantages of creating a dedicated object to handle your threaded code is that it allows complete freedom for executing a number of tasks in a set order. With delegates, you're forced to make each method call asynchronous. With a threaded object, you can create a series of method calls that must execute synchronously with respect to each other but can execute asynchronously with respect to the rest of the program. This is particularly useful if you need to call one method with the results of another method.

One simple example is to extend the StockQuoteLookup class to support a batch of lookups, as shown in Listing 6-25.

Listing 6-25 Adding support for multiple method calls
 Public Class StockQuoteLookup     Public StockCalls As New ArrayList()     Private Proxy As New StockQuoteService()     Public Sub DoLookup()         Dim StockCall As New StockQuoteInfo()         ' Iterate through the collection, executing each stock lookup         ' synchronously.         For Each StockCall In StockCalls             StockCall.PriceReturnValue = Proxy.GetStockQuote( _              StockCall.TickerParameter)         Next     End Sub End Class 

The client code can then create a StockQuoteLookup and add a list of the tickers that require quotes, as shown in Listing 6-26.

Listing 6-26 Calling a method multiple times asynchronously
 Dim Lookup As New StockQuoteLookup() Lookup.StockCalls.Add(New StockQuoteInfo("MSFT")) Lookup.StockCalls.Add(New StockQuoteInfo("AMZN")) Lookup.StockCalls.Add(New StockQuoteInfo("EGRP")) Dim LookupThread As New Thread(AddressOf Lookup.DoLookup) LookupThread.Start() 

In this case, making multiple asynchronous calls would probably still increase performance because the wait time is collapsed. However, this example illustrates how well a threaded object can wrap up all the required information in a single package.

Threading and User Interfaces

A threaded object provides an ideal way to call several required methods, but it doesn't solve one key problem. Namely, how should the thread signal that it's finished its work? In the threading examples used so far, the client has been forced to continuously poll the thread. When the thread has finished, it's safe to access the properties of the StockQuoteLookup class. This isn't of much use if you want to return control to the user so that other tasks can be initiated.

One crude solution would be to create a timer that fires in periodic intervals and then polls the in-progress thread. Alternatively, you could let the user dictate when polling should take place, perhaps checking for results whenever the user clicks a Refresh button. Both of these approaches are less than elegant.

A third possibility would be to let the threaded object modify the user interface directly. However, the only safe way that a control can be modified is from code that executes on the thread that owns the user interface. In a Windows application, this is the main thread of your application.

Fortunately, all .NET controls provide a special Invoke method (inherited from the base Control class) just for this purpose. You call the Invoke method and pass it a delegate that points to another method. This method is then executed on the correct user interface thread.

The next example shows a rewritten StockQuoteLookup class that implements this pattern to provide safe updating (as shown in Listing 6-27).

Listing 6-27 Marshaling calls to the user interface thread
 Public Class StockQuoteLookup     Public StockCall As New StockQuoteInfo()     Private Proxy As New StockQuoteService()     ' By using the base Control class, we have the freedom to use a      ' variety of controls, including labels, buttons,     ' and status bar panels.     Private ControlToUpdate As Control     Public Sub DoLookup()         StockCall.PriceReturnValue = Proxy.GetStockQuote( _          StockCall.TickerParameter)         ' Marshal the UpdateControl() code to the correct thread.         ControlToUpdate.Invoke( _           New MethodInvoker(AddressOf UpdateControl))     End Sub     ' This method will execute on the user interface thread.     Public Sub UpdateControl()         ControlToUpdate.Text = StockCall.TickerParameter & _          " = " & StockCall.PriceReturnValue.ToString()     End Sub     ' Use a default constructor to force the client to submit     ' the update control.     Public Sub New(control As Control)         ControlToCallback = Control     End Sub End Class 

Note that the method you pass to Invoke must match the MethodInvoker delegate (which means it can't have any parameters or a return value). In Listing 6-27, this method is UpdateControl.

Note

Interestingly, the Control class also provides BeginInvoke and EndInvoke methods that enable the user interface code to be executed asynchronously, which is rarely required. The Invoke, BeginInvoke, and EndInvoke methods are the only methods that are thread-safe in a Windows control. No other methods should be called from another thread.


You can easily verify that this code is working correctly by naming both threads and calling the MessageBox.Show method to display the thread name in the DoLookup and UpdateControl method. You'll verify that the DoLookup executes on the secondary thread, while UpdateControl executes on the main thread, which owns the user interface.

To use this code, just supply the control you want updated when you create the StockQuoteLookup object:

 Dim Lookup As New StockQuoteLookup(lblStockResult) 

Using Callbacks and Locking

One glaring problem in the example we just explored is that the user interface code is tightly coupled with the request processing logic. Ideally, the threading class shouldn't assume the responsibility of updating the client's user interface.

A more traditional approach is for the thread to notify the client that it has finished using an event or a callback. To accommodate this notification technique, you need to add a delegate that represents the callback or a new event to the thread class.

The next example updates the StockQuoteLookup class to use a delegate. Note that the delegate variable doesn't use the AsyncCallback signature that we used earlier because the threading class doesn't return an IAsyncResult object. Instead, it defines a more useful event-type syntax that passes a reference to the StockQuoteLookup instance and an empty EventArgs object, as shown in Listing 6-28.

Listing 6-28 A custom threading class that uses a callback
 Public Class StockQuoteLookup     Public StockCall As New StockQuoteInfo()     Private Proxy As New StockQuoteService() 
     ' Define callback.     Public Delegate Sub StockQuoteCallback(_      sender As StockQuoteLookup, e As EventArgs)     ' Define callback variable.     Public StockCallback As StockQuote     Public Sub DoLookup()         StockCall.PriceReturnValue = Proxy.GetStockQuote( _          StockCall.TickerParameter)         ' Notify the client (and pass instance of this object).         StockCallback(Me, New EventArgs())     End Sub     ' Use a default constructor to force the client to submit     ' a callback method.     Public Sub New(callback As StockQuoteCallback)         StockCallback = callback     End Sub   End Class 

The client submits the callback address when creating the custom threaded object:

 Dim Lookup As New StockQuoteLookup(AddressOf ReceiveResult) 

Alternatively, you can implement the same sort of logic by using a custom event in the StockQuoteLookup instead of storing a delegate. Listing 6-29 demonstrates this approach.

Listing 6-29 A custom threading class that fires a notification event
 Public Class StockQuoteLookup     Public StockCall As New StockQuoteInfo()     Private Proxy As New StockQuoteService()     ' Define the event.     Public Event Completed(sender As StockQuoteLookup, e As EventArgs)     Public Sub DoLookup()         StockCall.PriceReturnValue = Proxy.GetStockQuote( _ 
          StockCall.TickerParameter)         ' Fire the event.         RaiseEvent Completed(Me, New EventArgs())     End Sub End Class 

In this case, the client needs to hook up an event handler after creating the object:

 Dim Lookup As New StockQuoteLookup() AddHandler Lookup.Completed, AddressOf ReceiveResult 

Both versions are essentially equivalent. The only difference is that the event-handling version uses a "loosely coupled" design in which multiple clients can handle the same event. However, the StockQuoteLookup class won't be aware of which clients are listening.

Both versions also suffer from the same problem. The client callback method will execute on the caller's thread, meaning that you can directly touch any other application variables or modify the user interface. Fortunately, you can still use the Control.Invoke method to update the user interface safely. In this case, however, we need to take a further step. You can't directly pass the retrieved information to the method that will update the user interface, so you'll need to store it in a form-level variable first.

Listing 6-30 shows the complete solution.

Listing 6-30 A client that uses the custom threading class
 Public Class FormClient     Inherits System.Windows.Forms.Form     ' (All other code, including the code to start the threads,     ' is left out for clarity.)     Public ResultText As String     Public Sub ReceiveResult(sender As StockQuoteLookup, _      e As EventArgs)         ' Set the update text.         ResultText = sender.StockCall.TickerParameter & _          " = $" & sender.StockCall.PriceReturnValue.ToString()         ' Marshal the UpdateDisplay() code to the correct thread.         lblResults.Invoke( _           New MethodInvoker(AddressOf UpdateDisplay)) 
     End Sub     Public Sub UpdateDisplay()         ' Add the recently received text to a label control.         lblResults.Text &= vbNewLine & ResultText     End Sub End Class 

In the preceding example, we assume that only one thread can contact the application at a time with the result of a stock quote. If, instead, you have multiple requests performing multiple stock lookups and they can all return results at the same time, you need to track information in a collection and add locking. The rewritten example in Listing 6-31 adds this functionality using a Queue collection, which retrieves items in the same order they are added.

Listing 6-31 A client that uses the custom threading class with multiple simultaneous calls
 Public Class FormClient     Inherits System.Windows.Forms.Form     ' (All other code, including the code to start the threads,     ' is left out for clarity.)     Public Results As New System.Collections.Queue()     Public Sub ReceiveResult(sender As StockQuoteLookup, _      e As EventArgs)         ' Set the update text.         Dim ResultText As String         ResultText = sender.StockCall.TickerParameter & _          " = $" & sender.StockCall.PriceReturnValue.ToString()         ' Add the text to the queue.         SyncLock Results             Results.Enqueue(ResultText)         End SyncLock         ' Marshal the UpdateDisplay() code to the correct thread.         lblResults.Invoke( _           New MethodInvoker(AddressOf UpdateDisplay))     End Sub     Public Sub UpdateDisplay() 
         ' Add the recently received text to a label control.         Dim ResultText As String         SyncLock Results             ResultText = CType(Results.Dequeue(), String)         End SyncLock         lblResults.Text &= vbNewLine & ResultText     End Sub End Class 

This might seem slightly excessive for a modest client application, but it does demonstrate how to use locking and marshaling in the same method. This example also provides some more insight into the infamous question "Does locking harm performance?" A healthy dose of common sense can help you determine that in this case locking isn't a problem. The lock on the Results object is held for a very brief amount of time. Even if hundreds of stocks are being queried at the same time, the application can easily keep up.

The online samples for this chapter include a simple test program that uses this design and shows how all the pieces work together. It submits a batch of requests simultaneously to a stock quote service, which returns random price information. Figure 6-7 shows this application.

Figure 6-7. The asynchronous Windows client

graphics/f06dp07.jpg

Sending Instructions to a Thread

The preceding section showed an in-depth example of how a thread can communicate with the rest of your program by raising an event. Sending a message from your application to a worker thread is slightly different. Although a dedicated worker thread could handle an event, this option isn't common and is likely to lead you to tightly bind a thread to a particular user interface model. A better approach is to design the thread with built-in support for certain instructions.

Typically, a client has relatively few instructions to give a thread. The most common type of communication is to order a thread to end gracefully. You can provide this capability by adding a Boolean Stop property to the Thread class. The thread can then regularly check this value (perhaps after every pass in a loop) and end its processing if it is set to True, as shown in Listing 6-32.

Listing 6-32 A threaded class that recognizes a stop signal
 Public Class ProcessData     Public Stop As Boolean = False     Public Sub Process         Do Until Stop             ' (Carry out a repetitive processing task).         Loop     End Sub End Class 

If the thread fails to respond after a short interval of time after the client sets its Stop property to True, you can take the next, more drastic step, and abort the thread, as shown in Listing 6-33.

Listing 6-33 Sending a stop signal to a thread
  ' Attempt to stop the thread gracefully. TaskObject.Stop = True ' Wait for the thread for up to 10 seconds. TaskThread.Join(TimeSpan.FromSeconds(10)) ' Check if the thread has ended. If (TaskThread.ThreadState And ThreadState.Stopped) <> _  ThreadState.Stopped Then     ' The thread is still running. Now you must end it forcefully.     TaskThread.Abort()     TaskThread.Join() End If 

You might want more control over a thread, including the ability to tell a thread to abandon its current work and start processing new data. However, it doesn't make sense to add multiple thread properties to try to accommodate this need. If you do, you will force the application to know far too much about the structure of the Thread class. This makes it more difficult to enhance the Thread class and might introduce additional vulnerabilities if the Thread class properties are not set correctly.

A better approach is to create a dedicated method in the Thread class. The client can then call this method when needed. Listing 6-34 presents the outline for one example. In this case, the client can call the SubmitNewData method to instruct the thread to abort its current task and start processing different information. The SubmitNewData method sets a flag that indicates new data has arrived (the RestartProcess variable) and copies the submitted information to a safe place (the NewData variable).

Listing 6-34 A threaded class that can handle new information
 Public Class ProcessData     ' The data that is currently being processed.     ' The integer data type is used as a basic example.     Private Data As Integer     ' New data that should be processed as soon as possible.     Private NewData As Integer     ' Flags that indicate when processing should stop.     Private RestartProcess As Boolean     Private Stop As Boolean = False     Public Sub Process         Do Until Stop             Do Until RestartProcess Or Stop                 ' (Carry out a repetitive processing task).             Loop             ' Pick up the new data.             If RestartProcess And Not Stop Then                 Data = NewData                 RestartProcess = False             End If         Loop     End Sub 
     Public Sub SubmitNewData(data As Integer)         ' Enter the new data.         NewData = data         ' Set a flag that will alert the Process method that new         ' data exists.         RestartProcess = True     End Sub End Class 


Microsoft. NET Distributed Applications(c) Integrating XML Web Services and. NET Remoting
MicrosoftВ® .NET Distributed Applications: Integrating XML Web Services and .NET Remoting (Pro-Developer)
ISBN: 0735619336
EAN: 2147483647
Year: 2005
Pages: 174

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net