Page 5 of 5
For example, the previous code can be written in a more robust way as:
if (temp != count + 1)
count = temp;
Notice that while this is rather more foolproof than using the basic Monitor methods a thread that doesn’t play by the rules and simply accesses the resource will spoil everything.
The point is that you can’t enforce locking, just hope that everyone remembers to use it.
There are other Monitor methods that are sometimes useful.
For example the TryEnter method will attempt to acquire a lock, after waiting for a specified time, but will allow the thread to continue if the lock cannot be acquired.
Clearly in this case you need to test the return value (a Boolean) to see if the lock has been acquired and do something different if it hasn’t. The Wait method will allow the thread that currently has the lock to free it and allow other threads to acquire it while it waits for it to be signalled by another thread attempting to acquire the lock again. Another thread, one that currently has the lock, can signal to the next waiting thread (or to all waiting threads) to try to acquire the lock by using the pulse or pulseall method.
To understand how this might be used consider a thread that processes a buffer that is filled by another thread. The processing thread can call wait when it has finished processing the buffer and allow the filling thread to access it. As soon as the filling thread has finished its work it can use pulse to tell the processing thread to try to acquire the lock and start work again.
The clever part is that this mechanism generalises to multiple work-creating and work-consuming threads and they can all queue in an orderly fashion to access the resource using wait and pulse.
There are other problems with locking and the most celebrated is perhaps the deadlock condition.
Put simply, if thread A locks resource one and thread B locks resource two everything is fine unless thread A also wants a lock on resource two before it can complete and if thread B needs a lock on resource one before it can complete.
The result is that both threads spend forever waiting for the other to finish and release the resource.
This is deadlock and it can occur in much more complicated ways than this simple “A waits for B which waits for A” situation.
It is possible to create a deadlock ring of dependency by having A wait for B, which waits for C, which waits for D which is waiting for A.
There isn’t much you can do about deadlock except to be aware of it and design your access strategies with a great deal of care. You can try to avoid locking threads on more than one lock at a time but this can slow things down to unacceptable levels as threads have to wait while another thread acquires an oversized lock on resources, some of which it isn’t actually using.
A better strategy is to attempt to acquire all of the locks that a thread needs to complete at the start and release any that have been acquired if it isn’t possible to acquire them all. Again this can result in a loss of performance.
The bottom line is that multi-threading with locks isn’t easy and carries the seeds of disaster. Multithreading without locks is easy but is always guaranteed to be a disaster.
Beyond the Monitor
You can do most of what you need to with nothing but the Monitor but .NET does provide other locking facilities.
For example the Mutex provides locking across process boundaries and the Semaphore can be used to control the number of threads that can access a resource. All of these work in similar ways to the Monitor and you should have no problems in understanding how they work – but if the Monitor does the job then use it.
There are also new facilities for creating parallel programs within .NET but these to have their problems. Lookout for a future article on the topic.
If you would like to be informed when the article appears subscribe to I Programmer or follow us on Twitter or Facebook.