Singleton Basics

The lifetime of a typical singleton object is not tied to the client. Consider .NET Remoting, for instance. When a client makes a call, the .NET Remoting service provides a thread from its pool, which then accesses the object. Multiple clients can access the same object on multiple different threads if they make concurrent calls.

If the singleton object is stateless (in other words, it doesn't provide any class properties or member variables), there's really nothing to worry about. Any variables created in a method body are local to the current call. In this case, a singleton object performs like a single-call object. In fact, contrary to what you might expect, a singleton object used in this way might even perform better because it doesn't need to be destroyed and re-created for each call. More likely, however, you'll use a singleton design only if you need to maintain some sort of shared information.

One trivial example is a component that tracks usage information. This logging can be integrated directly into each method or can be applied using a layered interception design. Consider Listing 7-1, in which ClassA tracks usage information and ClassB performs the actual task.

Listing 7-1 Using interception with a singleton
 Public Class ClassA     Inherits MarshalByRefObject     ' Tracks the number of requests from all clients since the class     ' was created.     Private NumberOfCalls As Integer = 0     Public Function DoSomething() As Integer         ' Perform usage logging in a thread-safe manner.         System.Threading.Interlocked.Increment(NumberOfCalls)         ' Execute the method that does the work.         Dim TaskB As New ClassB()         Return TaskB.DoSomething()     End Function     Public Function GetUsageInformation() As Integer         Return NumberOfCalls     End Function End Class Public Class ClassB     Public Function DoSomething() As Integer         ' (Code goes here.)     End Sub End Class 

In this case, the client never directly accesses ClassB. Instead, it starts a task by calling a method in ClassA. This method is really just a front for the functionality in ClassB. This layered approach is shown in Figure 7-3.

Figure 7-3. Layered singleton design

graphics/f07dp03.jpg

In a more sophisticated example, the GetUsageInformation method might return a collection listing the number of times various methods have been called or might return a custom structure with several pieces of aggregate information. Keep in mind that retaining this information in memory is a good example of how a singleton class works; it isn't a good design choice. In this case, logging information to an external resource (such as a database or event log) is more scalable and durable because the information won't be lost if the component crashes.

Tracking Clients

Another common reason to use the singleton design pattern is to retain client-specific information or even to allow clients to interact. Tracking clients serves two purposes. It enables you to create components that can report on the current user load, or return information about what tasks are currently underway and what clients are connected. To add similar functionality to stateless remote objects, you need to create some sort of shared event log to which each component writes information. The other reason to track clients is to create asynchronous worker services. In this design, clients submit a request for a time-consuming task by calling a method such as BeginTask and pick up the results later by calling a method such as RetrieveTaskResult. This design can be implemented in an XML Web service or .NET Remoting scenario.

To track clients, ClassA needs some sort of collection object. Typically, the System.Collections.Hashtable class is the best choice because it can hold any type of object and allows for fast lookup based on a key. This key is some sort of unique value that identifies the client. What you actually store in the Hashtable varies, but it might be an instance of the worker ClassB that is serving the particular client.

Listing 7-2 shows the bare outline of one possible structure.

Listing 7-2 Client tracking with an asynchronous singleton
 Imports System.Collections Imports System.Threading Public Class ClassA     Inherits MarshalByRefObject     ' Tracks clients using this singleton.     Private ClientTasks As New Hashtable()     Public Sub StartSomething(clientID As String)         ' Check if the client already has a task object.         ' If not, create it.         Dim Task As ClassB = ClientTasks(clientID)         If Task Is Nothing Then             Task = New ClassB()             ClientTasks(clientID) = Task         End If         ' Check if the task is already in progress.         If Task.InProgress Then             Throw New InvalidOperationException("Already in progress.")         Else             Task.InProgress = True             Task.Client = clientID             ' Start the process on another thread.             Dim TaskThread As New Thread(AddressOf Task.DoSomething)             Task.ExecutingThread = TaskThread             TaskThread.Start()                     End If     End Sub     Public Function RetrieveResult(clientID As String) As Integer         ' Check if the task exists and is completed.         Dim Task As ClassB = ClientTasks(clientID)         If Task Is Nothing Then             Throw New InvalidOperationException("No task exists.")         ElseIf Task.InProgress Then             Throw New InvalidOperationException("Still in progress.")         Else             Return Task.Result         End If     End Function    
  Public Sub ReleaseTask(clientID As String)         Dim Task As ClassB = ClientTasks(clientID)         If Not (Task Is Nothing) Then             If (Task.ExecutingThread.ThreadState And _              ThreadState.Stopped) <> ThreadState.Stopped                 ' We must manually abort the thread.                 Task.ExecutingThread.Abort()                 Task.Join()             End If             Task = Nothing             ClientTasks.Remove(clientID)         End If          End Sub End Class Public Class ClassB     Public InProgress As Boolean     Public Result As Integer     Public ExecutingThread As Thread     Public Client As String     Public Sub DoSomething()         ' (Code goes here to calculate Result.)         ' Notify when finished.         InProgress = False             End Sub End Class 

ClassA acts a dispatcher, creating ClassB instances as required and starting them on new threads. If the client calls RetrieveResult before the task completes, ClassB doesn't wait. Instead, an InvalidOperationException is thrown automatically. To get around this limitation, you might want to enhance ClassB to include not only a Result member variable but also a Progress field. The client can then query the progress through a method such as CheckStatus in ClassA. Alternatively, it might be a more friendly approach to just return -1 if the result is not yet complete. In either case, you face a common limitation of asynchronous designs: client notification. In an XML Web service, there is no way around this problem because the client cannot accept messages except those returned from a Web method. In a .NET Remoting component, you can use a bidirectional channel and configure the client as a listener, as described in Chapter 4. ClassB will then fire an event to ClassA to notify that a task had finished, and ClassA will notify the client with a callback or another event.

Managing and Cleaning Up Tasks

Our example assumes that the client will release ClassB when it completes by calling ReleaseTask. If not, the object remains floating in memory endlessly, eventually crippling the server and requiring a server application restart. In our example, the problem is minor because the memory used by ClassB is extremely small; this might not always be the case, however, particularly if the component is returning a DataSet with a full set of information.

You can improve on this situation by automatically releasing the object when the client retrieves the result, but this still doesn't help if the client disappears and never retrieves the result at all. This is simply not acceptable for a distributed application. To get around it, you need to code your own logic for monitoring in-progress tasks.

A good starting point is to refine the worker class so that it stores the time that the operation began, as shown in Listing 7-3.

Listing 7-3 Tracking the task start time
 Public Class ClassB     Public InProgress As Boolean     Public Result As Integer     Public ExecutingThread As Thread     Public Client As String     Public StartTime As DateTime     Public Sub DoSomething()         StartTime = DateTime.Now         ' (Code goes here to calculate Result.)         ' Notify when finished.         InProgress = False             End Sub End Class 

Listing 7-4 shows a sample method that iterates through the Clients collection continuously, checking whether any objects are more than 1 hour old.

Listing 7-4 Monitoring in-progress tasks
 Public Sub CheckClients()     Dim Limit As TimeSpan = TimeSpan.FromHours(1)     ' Perform the check.     Do         ' Pause briefly.         Thread.Sleep(TimeSpan.FromMinutes(10))         Try             ' Iterate through the collection.             Dim Task As ClassB             Dim Item As DictionaryEntry             For Each Item in ClientTasks                 Task = CType(Item.Value, ClassB)                 If DateTime.Now.Subtract(ClassB.StartTime).TotalMinutes > _                   Limit.TotalMinutes Then                     ReleaseTask(ClassB.Client)                 End If             Next         Catch             ' By catching the error, we neutralize it.             ' The check restarts in ten minutes.             ' It is a good idea to also log the error, in case             ' it is a recurring one that is hampering performance.         End Try     Loop End Sub 

There are two important points about this routine:

  • It uses the shared Thread.Sleep method to pause execution for 10 minutes after each full check to prevent it from using too many system resources. (Depending on the number of clients, it might make more sense to pause during the iteration, but this is more prone to error if the contents of the collection change while the thread is paused.)

  • The entire method is enclosed inside a Try/Catch block. This ensures that it can restart the monitoring process even if an error is encountered. Possible errors include checking a StartTime value before it is initialized or trying to move to an item in the collection just as it is being removed. Because the CheckClients method runs continuously, it is not appropriate to use locking on the Clients collection because this would effectively stall other users as they try to register with ClassA.

This code should execute on a separate thread, which can be tracked by a member variable:

 Private MonitorThread As Thread 

You start this method when ClassA is first created:

 MonitorThread = New Thread(AddressOf Me.CheckClients) MonitorThread.Priority = ThreadPriority.BelowNormal ' This ensures the thread stops when the main thread terminates. MonitorThread.IsBackground = True MonitorThread.Start() 

Ticket Systems

In the example so far, we've assumed that each client has a unique ID value and that this ID is submitted for every request. This might be the case in a controlled environment in which every user of the component is known. However, this is not likely in a large-scale distributed application. It also exposes obvious potential problems. Clients who submit the wrong ID number might retrieve the results of other users, for example, and there is no way for a single client to start more than one task at a time.

A more common design is to use a ticket system. In this case, a random key is generated for the client when the client first starts the task or logs in to the remote component. You can construct the ticket in a number of ways. One possibility is to use a sequential number. You then keep track of the last issued number in a member variable in ClassA. Every time a client makes a request, you increment the number and then use it. To ensure that two clients can't try to increment the number at the same time (and accidentally lead to two clients with the same key), you can use the System.Threading.Interlocked class to increment the number, as explained in the preceding chapter. Another approach is to use a random number that has little chance of being duplicated. You can create a number based on the current time (including seconds), for example, and add several random digits to the end.

Both of these ticket-generating systems have a fundamental security flaw, however. With a sequential numbering system, a malicious client can hijack another client's session by guessing the next number. This risk is slightly lessened in the random number situation; because the number is based on the current time, however, a user could guess a valid ticket by issuing several hundred calls. A better approach is to use a statistically unique value such as a globally unique identifier (GUID) for your ticket. GUID values are not related to one another in any kind of sequence and are therefore much more difficult to guess. They correspond to 128-bit integers and are commonly written as a string of lowercase hexadecimal digits in groups of 8, 4, 4, 4, and 12 digits, separated by hyphens. An example is 382C74C3-721D-4F34-80E5-57657B6CBC27. In .NET, you can generate a new GUID by using the NewGuid method of the System.Guid structure.

Listing 7-5 shows a rewritten StartSomething method that uses this technique. Note that it returns the generated key. The client is responsible for holding on to this key and submitting it to the RetrieveResult method.

Listing 7-5 Asynchronous tasks with tickets
 Public Function StartSomething() As String     ' Generate new ticket.     Dim Ticket As String = Guid.NewGuid().ToString()     ' Create task and add it to the collection.     Dim Task As New ClassB()     ClientTasks(Ticket) = Task     ' Start the process on another thread.     Task.InProgress = True     Task.Client = Ticket     Dim TaskThread As New Thread(AddressOf Task.DoSomething)     Task.ExecutingThread = TaskThread     TaskThread.Start()     Return Ticket             End Function 

Note

Ticket-based systems are also used to reduce the cost of authenticating users. This important security technique is covered in depth in Chapter 13. The case study in Chapter 18 also presents ticket-based authentication in a full example and shows how you can create an asynchronous task-based system that doesn't require the complexity of singleton objects.


Locking

The example presented so far is safe only for a single user. To support multiple users, you need to add locking for any shared variables. In this case, the ClientTasks collection is the only piece of information shared with all clients.

Before you add locking, it helps to know a little bit about how the Hashtable works. The Hashtable is designed to support multithreaded access, meaning that an unlimited number of users can read values from the collection at the same time. However, two collection operations aren't considered thread-safe:

  • Writing to the collection

    This includes adding a new value or overwriting or removing an existing one.

  • Enumerating through the collection

    Because entries can be added or removed while the enumeration is in progress, you might encounter an error (but not data corruption).

To solve the first problem, you just lock the collection before adding the new client task or removing an existing task (as shown in Listing 7-6).

Listing 7-6 Managing concurrency with locking
 Public Function StartSomething() As String     ' Generate new ticket.     Dim Ticket As String = Guid.NewGuid().ToString()     ' Create task and add it to the collection.     Dim Task As New ClassB()     SyncLock ClientTasks         ClientTasks(Ticket) = Task     End SyncLock     ' Start the process on another thread.     Task.InProgress = True     Task.Client = Ticket     Dim TaskThread As New Thread(AddressOf Task.DoSomething)     Task.ExecutingThread = TaskThread     TaskThread.Start()     Return Ticket             End Function Public Sub ReleaseTask(clientID As String)     Dim Task As ClassB = ClientTasks(clientID)     If Not (Task Is Nothing) Then         If Task.ExecutingThread.ThreadState And _          ThreadState.Stopped <> ThreadState.Stopped             ' We must manually abort the thread.             Task.ExecutingThread.Abort()             Task.ExecutingThread.Join()         End If         Task = Nothing         SyncLock ClientTasks             ClientTasks.Remove(clientID)         End SyncLock     End If      End Sub 

Note that this code does not lock the client's Task object. It is theoretically possible for more than one client to access the same task (by using the same ticket), but there is no practical reason for this to occur. It's also not necessary to add locking to the CheckClients method. Adding this locking can drastically slow down performance because all the new clients would be locked out while the entire collection is scanned. Instead, the CheckClients method is prepared to handle an exception resulting from an invalid read.

Instead of using the SyncLock statement, you can access a thread-safe wrapper on the collection by using the Hashtable.Synchronized method.

 HashTable.Synchronized(ClientTasks).Remove(clientID) 

The synchronized version of the collection is just a class that derives from Hashtable and that adds the required locking statements. You can easily add this support to your own objects just by creating a derived thread-safe class from an ordinary class.

You might expect that the thread-safe collection would offer the best performance because its locking code has been optimized as required. However, this isn't always the case. For example, if you perform several operations with a synchronized collection, it will acquire and release a lock for each separate operation. If you deal with the basic collection, however, you can acquire a lock and perform a batch of operations all at once, improving overall performance.

Finally, as a matter of good form, it's recommended that when locking a collection that you actually lock the object returned by the Hashtable.SyncRoot property. This ensures that even if you are locking on a thread-safe wrapper (such as the one returned through the Synchronized property), the underlying collection is still locked, not just the wrapper:

 SyncLock ClientTasks.SyncRoot     ' (Do something.) End SyncLock 
Advanced Reader-Writer Locking

With collections, you have a better locking choice. You can use the ReaderWriterLock from the System.Threading namespace, which is optimized to allow a number of readers while locking down write access to a single user. The class provides methods for acquiring and releasing reader and writer locks and for upgrading from a reader lock to a writer lock. The methods for acquiring a lock accept a TimeSpan that indicates the maximum wait time.

If you know that your object supports the reader-writer lock semantics, you can dramatically improve concurrency and reduce thread contention by changing from SyncLock (and the Monitor class) to a ReaderWriterLock. First of all, you need to create a member variable in the class that represents the lock:

 Private ClientTasksLock As New ReaderWriterLock() 

Listing 7-7 shows the StartSomething and RetrieveResult methods rewritten in this fashion.

Listing 7-7 Using reader-writer locking
 Public Function StartSomething() As String     ' Generate new ticket.     Dim Ticket As String = Guid.NewGuid().ToString()     ' Create task and add it to the collection.     Dim Task As New ClassB()     ' Specify an infinite wait with the  1 parameter.     ClientTasksLock.AcquireWriterLock(-1)     ClientTasks(Ticket) = Task     ClientTasksLock.ReleaseLock()     ' Start the process on another thread.     Task.InProgress = True     Task.Client = Ticket     Dim TaskThread As New Thread(AddressOf Task.DoSomething)     Task.ExecutingThread = TaskThread     TaskThread.Start()             End Function Public Function RetrieveResult(clientID As String) As Integer     ClientTasksLock.AcquireReaderLock(-1)     Dim Task As ClassB = ClientTasks(clientID)     ClientTasksLock.ReleaseLock()     If Task Is Nothing Then         Throw New InvalidOperationException("No task exists.")     ElseIf Task.InProgress Then         Throw New InvalidOperationException("Still in progress.")     Else         Return Task.Result     End If End Function 


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