Locking

The simple threading example we've created so far works well because the two tasks work independently. If you need to communicate between threads, life is not as simple. Trying to access an object on more than one thread at once is inherently dangerous. Threading problems are also infamously hard to diagnose because a single command in a high-level language such as Visual Basic .NET might actually compile to dozens of low-level machine-language instructions.

Locking enables you to obtain exclusive access to an object that's used by another thread. You can then safely modify the value without worrying that another thread might try to read or change it while your operation is in progress. Locking is built into Visual Basic .NET through the SyncLock statement. To demonstrate locking, we'll consider a simple example of a server-side object. The same technique applies when you design multithreaded clients; before we look at a full client example, however, we need to consider a couple of additional details.

Consider an object exposed through .NET Remoting as a singleton. This object maintains a collection of information supplied by clients in an ArrayList. Clients add a piece of information through the AddInformation method. If several clients call this method simultaneously, however, an error can occur, which will likely lead to losing at least one client's information (as shown in Listing 6-12).

Listing 6-12 A singleton object without locking support
 Public Class InformationStorage     Inherits MarshalByRefObject     Private Information As New ArrayList()     Public Sub AddInformation(info As String)         Information.Add(info)     End Sub End Class 

To solve this problem, the AddInformation method can obtain an exclusive lock on the ArrayList before it adds the object using the SyncLock statement, as shown in Listing 6-13.

Listing 6-13 A singleton object with locking
 Public Class InformationStorage     Inherits MarshalByRefObject     Private Information As New ArrayList()     Public Sub AddInformation(info As String)         SyncLock Information             Information.Add(info)         End SyncLock     End Sub End Class 

This approach efficiently solves the problem. The first client will obtain a lock on the Information collection. When another client attempts to access the Information collection, the CLR forces it to pause and wait until the lock is released. The CLR then allows the next waiting client to create its lock and gain exclusive access to the collection. You don't need to worry about keeping track of who made the first call; the CLR takes care of this queuing automatically and notifies clients as an object becomes available.

Locking is a common solution that guarantees data integrity with stateful components that are shared by multiple clients. A common question that developers have with locking is whether it reduces performance. Technically, locking always entails some slowdown because it forces clients to wait, effectively making a multithreaded program act like a single-threaded program for a short interval of time. However, the amount of performance slowdown varies dramatically depending on how coarse the lock is, how long it's held for, how many methods use the locked object, and how many clients are using the class that contains the locked object. In the preceding example, the slowdown would be noticeable if hundreds of clients were continuously adding information. However, the situation is dramatically different with the next example (shown in Listing 6-14), which uses SyncLock to safeguard a global counter that records how many times a method is called.

In this case, we can't use the SyncLock statement with the GlobalCounter variable because SyncLock works only with reference types, not value types. The only alternative is to lock the entire current instance of the CountInvocations class. This means that clients will not be able to call any methods of the CountInvocations object. Clearly, this lock is much coarser and has a less desirable effect on user concurrency.

Listing 6-14 A coarser lock
 Public Class CountInvocations     Inherits MarshalByRefObject     Private GlobalCounter As Integer     Public Sub IncrementCounter()         SyncLock Me             GlobalCounter += 1         End SyncLock     End Sub End Class 

Incidentally, you can solve this problem in two ways. One easy solution is to replace the GlobalCounter variable with an object that exposes an integer property. For example, an instance of the following Counter class can be locked:

 Public Class Counter     Public Count As Integer End Class 

Another possible solution is to use the specialized System.Threading.Interlocked class, which is designed to solve just this sort of problem with incrementing or decrementing an ordinary integer variable. The Interlocked class provides shared methods that increment or decrement a variable in a thread-safe manner as a single atomic operation, as shown in Listing 6-15.

Listing 6-15 Using the interlocked class
 Public Class CountInvocations     Inherits MarshalByRefObject     Private GlobalCounter As Integer     Public Sub IncrementCounter()         Interlocked.Increment(GlobalCounter)     End Sub End Class 

Tip

You might wonder why you need to lock the global counter in the first place. The problem is that the ordinary increment operation is actually shorthand for referring to two separate operations: reading a value and writing a value. Without synchronization, two clients might overlap. For example, they might both read the current counter value (say, 4) and both try to increment it to the next value (5). The end result is that the counter will be set to 5 twice rather than its rightful value of 6.


Race Conditions

With locking, you risk a couple of new problems. The first problem, race conditions, is really just a matter of not using the correct locking.

The example in Listing 6-16 shows the essence of a common race condition problem. A ProcessValue method obtains a lock when reading and writing a value, but it doesn't hold on to the lock for the time-consuming processing stage. This improves performance but raises the chance that another user will modify the value while the process is taking place.

Listing 6-16 Potential grounds for a race condition
 Public Class CountInvocations     Inherits MarshalByRefObject     Private Value As Single     Public Sub ProcessValue()         SyncLock Me             Dim ValueToProcess As Single = Value         End SyncLock         ' (Process ValueToProcess here).         SyncLock Me             Value = ValueToProcess         End SyncLock     End Sub End Class 

Race conditions can also be caused by unnoticed dependencies (for example, setting value A based on value B and C while another client is changing value C) or by the interference of more than one method.

There's no generic way to avoid a race condition. In the preceding situation, it might make sense to check that the current value equals the initially read value before making the update. Another choice is to use a Boolean member variable to keep track of whether the value is currently being processed and refuse to continue if it is.

Deadlocks

Whereas race conditions can lead to failed updates, deadlocks can stop a component from doing anything at all. Deadlocks are caused when two or more threads wait for each other to release a resource. For example, consider the two methods in Listing 6-17.

Listing 6-17 A likely candidate for a deadlock
 Public Sub MethodA()     SyncLock ObjectA         SyncLock ObjectB             ' (Do something with A and B).         End SyncLock     End SyncLock End Sub Public Sub MethodB()     SyncLock ObjectB         SyncLock ObjectA             ' (Do something with A and B).         End SyncLock     End SyncLock End Sub 

Assume MethodA gets a hold of ObjectA and then tries to obtain a lock on ObjectB. In the meantime, MethodB obtains exclusive access to ObjectB and tries for ObjectA. Both methods will wait for each other to release an object, with neither method giving in. In this example, the culprits are nested SyncLock blocks. However, this situation is just as likely with separate objects that are trying to obtain locks on each other.

One of the problems with detecting and solving deadlocks is that they might not happen very often. (In fact, they might rely on a client calling just the right combination of methods.) The only way to avoid deadlock situations is through careful programming. Keep the following few guidelines in mind:

  • Hold locks for as short a time as possible. Don't enclose code statements in a SyncLock block that don't need exclusive access to the object.

  • Always obtain locks in the same order. If both MethodA and MethodB need ObjectA and ObjectB, for example, make sure they obtain locks in the same order. If that approach were used in the preceding example, one method would complete successfully while the other would wait politely.

  • If you can't complete all your locks, release all your locks. In other words, if you achieve a lock on ObjectA but can't obtain one for ObjectB, release the lock on ObjectA and wait for a moment. This is the most difficult guideline to implement, and it can't be performed with the SyncLock statement alone. Instead, you need to have more fine-grained control of locks through the Monitor class.

Advanced Locking with the Monitor Class

The SyncLock statement is essentially a wrapper for another .NET class: System.Threading.Monitor. Monitor provides shared methods that enable you to test for locks, obtain locks, release locks, and notify other clients waiting for a locked object. Table 6-4 lists the methods of the Monitor class.

Table 6-4. Monitor Methods 

Method

Description

Enter

Acquires a lock on an object. This action also marks the beginning of a critical section. Any other thread that attempts to access this object will block until the object is free.

TryEnter

Attempts to gain a lock on an object or times out after a specified number of seconds. Returns True if the lock was acquired or False if it wasn't.

Exit

Releases the lock on the object. This action marks the end of the critical section.

Wait

Releases the lock on the specified object and waits to acquire a new lock. An overloaded version of this method enables you to specify a timeout.

Pulse and PulseAll

Sends a signal to one or more waiting threads. The signal notifies a waiting thread that the state of the object has changed and the owner of the lock is ready to release the lock. The waiting thread is placed in the object's ready queue so that it might receive the lock for the object.

You can rewrite a block of SyncLock code using the Monitor class. For example, Listing 16-18 shows how to convert a SyncLock block into code that uses the Monitor class:

Listing 6-18 SyncLock-equivalent code with the Monitor class
 ' SyncLock code. SyncLock Obj     ' (Use object here.) End SyncLock ' Monitor equivalent of SyncLock code. Try     Monitor.Enter(Obj)     ' (Use object here.) Finally     Monitor.Exit(Obj) End Try 

In other words, the SyncLock statement uses exception-handling code that ensures that even if an error is generated, the lock is released on the object. This is a basic level of failsafe code required in any distributed application to prevent deadlocks.

In addition, you can make an intelligent attempt to acquire a lock using TryEnter, as shown in Listing 6-19.

Listing 6-19 Testing for a lock
 If Monitor.TryEnter(Obj, TimeSpan.FromSeconds(60)) Then     ' We have exclusive access to Obj.     Try         ' (Use object here.)     Finally         Monitor.Exit(Obj)     End Try Else     ' After 60 seconds, the lock could still not be created.     ' Log the error and thrown an exception to alert the client. End If 

This enables you to implement another safeguard against deadlocks. Namely, if you need to acquire a lock on more than one object, you should use TryEnter on the second object. If you can't acquire your second lock, you should release the first lock and try again later. This pattern is demonstrated in Listing 6-20.

Listing 6-20 Avoiding deadlocks
 ' The TaskCompleted flag tracks is set once the operation is ' performed. Dim TaskCompleted As Boolean = False Do Until TaskCompleted     ' Attempt to get the first lock for 10 seconds.     If Monitor.TryEnter(ObjectA, TimeSpan.FromSeconds(10)) Then         ' We have exclusive access on ObjectA.         ' Attempt to get the second lock for 10 seconds.         If Monitor.TryEnter(ObjectB, TimeSpan.FromSeconds(10)) Then             ' We have exclusive access on ObjectA and ObjectB.             ' (Perform task with ObjectA and ObjectB.)             Monitor.Exit(ObjectB)             TaskCompleted = True         End If         ' This releases the first lock, regardless of whether the         ' task was completed.         ' This ensures that other classes that may need both objects         ' can finish their work.         Monitor.Exit(ObjectA)       End If     If Not TaskCompleted         ' The task will attempted in the next loop iteration.         ' First, the code will pause for 60 seconds.         Thread.Sleep(TimeSpan.FromSeconds(60))     End If Loop 
Wait, Pulse, and the Provider-Consumer Problem

The Wait and Pulse methods are designed to solve the classic provider-consumer­ problem. This occurs when one thread (a consumer) needs to acquire information from another thread (the provider). The consumer can use Wait.Enter to gain an exclusive lock on the provider and check for the data it needs. If the provider still hasn't created the data, however, acquiring this lock will freeze it, ensuring that the data is never created. In this case, the consumer calls the Wait method to move to the wait queue for the consumer object. This indicates that it wants to eventually obtain an exclusive lock on the provider. The provider then calls the PulseAll method to notify all the waiting consumers that the data is ready. The consumer threads then move to the ready queue and take turns acquiring the exclusive locks they need to read information from the provider.

Note that to use Wait and PulseAll, you must call Enter to start a synchronized block. The provider-consumer problem rarely appears in a distributed application because .NET handles low-level infrastructure details such as creating class instances for a client and managing remote object lifetime. (These details are beyond the scope of this book.)



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