Deep C# - Threading,Tasks and Locking |
Written by Mike James | ||||||||
Wednesday, 22 October 2025 | ||||||||
Page 4 of 7
Exclusion Using LockingIn principle, exclusion is simple to implement. All you need is a flag that a thread can test to see if the resource is in use. If it is then the thread should wait, forming a queue if needs be, until the resource is free as indicated by the flag. In practice implementing the flag so that nothing goes wrong is difficult in the extreme. If you simply use a Boolean as a flag, for example, consider what happens when two threads test it at more or less the same time to discover that it is set to false and both proceed to set it to true and use the resource it guards. Not only do you now have two threads using the resource, when one of them has finished it will set the flag back to false and allow other threads to use the resource. .NET provides a great many different locking facilities – so many that it’s very confusing. Part of the reason for this excess is that the theory of locking has been developed by many different people who each invented their own favorite way of doing the job. Let’s start with the simplest and most useful locking mechanism, the monitor, invented by Per Brinch Hansen in 1972. Every object in .NET has a monitor associated with it. A thread can acquire or enter the monitor only if no other thread has already acquired it. If it can’t acquire the monitor it simply waits, in a queue of other threads trying to acquire the same monitor, until the monitor is available. When the thread that has the monitor is finished it has to explicitly exit or release the monitor. The monitor is implemented in such a way that the sort of problems described with simple locking on a flag cannot happen – acquiring a monitor is an atomic operation. However, there are other things that can go wrong and the first big problem that confronts any programmer wanting to use a monitor is which object to use for locking. Remember every object has a monitor and so can provide a unique lock restricting access to a resource. At this point you need to focus on the fact that in many ways it is the code which accesses the resource which is locked and not the resource. Suppose we have a block of code that manipulates a global variable, we clearly don’t want this code to be active on more than one thread at a time so we acquire the lock at the start of the code and release it at the end of the block. If there are multiple different blocks of code that access the same resource then each of these blocks has to be written to acquire the lock at the start of the code and release it at the end. You can now see that, whatever object you use to provide the lock, it has to be accessible to all of the blocks of code that need to use it. The object that you place a lock on also has to be a fairly obvious one for the job and it shouldn’t be used, by accident, by another block of code to restrict access to another resource. There is a great deal of custom and practice in which objects should be used. For example, it is often said that you should lock on this for instance methods and a type object for static methods. The logic is that each instance method will only access resources that belong to that instance and so thread locking only has to be specific to that instance. However, a static method is common to all instances and hence likely to need locking so that one thread can only access it at a time, irrespective of which instance it is called from. These conventions have some sense, but an equally good, and some might argue better, approach is to create and use objects specifically to be used to lock a resource. Let’s see how this works. First, we need an object to use as a lock: static readonly object MyCountLock = new object(); As we want to access the object from everywhere it needs to be static and as we don’t want anyone to change it set to readonly. To make use of it we have to modify the count method quoted earlier to read: public void A() { Monitor.Enter(MyCountLock); for (int i = 0; i < 10000000; i++) { count++; } Monitor.Exit(MyCountLock); } public void B() { Monitor.Enter(MyCountLock); for (int i = 0; i < 10000000; i++) { count++; } Monitor.Exit(MyCountLock); } The calls to the static Monitor object acquire and release the lock using our object. If one of the threads tries to enter the monitor while the other thread hasn’t exited then it will wait for the other to exit. To try this out we need the same main program as before. If you place a breakpoint on the return you will see that the update is performed in an orderly fashion in the sense that neither thread interrupts the other during the update and no updates are lost. If you comment out either of the calls to the monitor then you will immediately see that the two threads do interfere with one another. Notice that the placement of the lock is important. In the example above the resource is locked for the entire for loop – that is the first thread will complete before the second gets a chance to update the variable. Locks should be acquired as late as possible and given up as soon as possible: public void A() { for (int i = 0; i < 10000000; i++) { Monitor.Enter(MyCountLock); count++; Monitor.Exit(MyCountLock); } } public void B() { for (int i = 0; i < 10000000; i++) { Monitor.Enter(MyCountLock); count++; Monitor.Exit(MyCountLock); } } This works and it only locks the variable while it is being updated. This allows both threads to work together, but at the expense of a lot more locking and unlocking operations. If you don’t want to use a specially created object then you can use the more commonly encountered lock on this: public void A() { for (int i = 0; i < 10000000; i++) { Monitor.Enter(this); count++; Monitor.Exit(this); } } The other thread needs to be changed in the same way. |
||||||||
Last Updated ( Wednesday, 22 October 2025 ) |