Being Threadsafe - An Introduction to the Pitfalls of Parallelism
Written by Mike James   
Friday, 31 May 2024
Article Index
Being Threadsafe - An Introduction to the Pitfalls of Parallelism
Making a thread safe
Concurrent exectution and exclusion
Locking in practice
Deadlock

Re-entrant code

Re-entrant code can be executed concurrently because each time it is run essentially a new copy of everything it uses is automatically created.

Re-entrancy is the key idea in functional programming. In F# here it is the norm, but in most languages you have to put in some work to achieve it. In C# you can create a re-entrant function in a number of ways but essentially you have to avoid using global non-constant data.

To convert our two example functions to be re-entrant all we have to do is remove the use of the global variable count. Once defined as local to each function the variables are allocated on the stack and hence local to each thread as each thread has its own stack.

Of course this means that the two functions cannot interact with each other, but this is in the nature of re-entrant functions.

A re-entrant function only uses local variables, accepts inputs and outputs only via parameters and return values. In other jargon a re-entrant function has no side-effects or is "pure".

Consider now the possibility that a class wants to be re-entrant, i.e. have nothing but re-entrant methods.

This is fine as long as it doesn’t make use of global or static variables. A static variable is just a special case of a global variable, i.e. it’s global to all instances of the class. If you still want your class to be re-entrant then you have no choice but to use thread-local storage.

The simplest way of doing this is to use the [ThreadStatic] attribute. For example to make the public count variable in the previous example thread-local we simply have to change its declaration to:

[ThreadStatic] static public int count;

Notice that now it’s also a static variable shared by all instances of the class but it isn’t shared by different threads executing methods in the same or different instances of the class.

Notice this solves the problem created by method A and B in the earlier example, Now the result of displaying count in the TextBox is always zero. The reason is that the thread that runs the class has its own copy of count as do each of the threads created to run method A and B. Also notice that there is no communication between any of the threads via the count variable - it really is a different variable on each thread.

The [ThreadStatic] attribute is the most efficient way of creating thread-local storage.

You can also do it the hard way using named or unnamed data slots but you need to keep in mind that there are less efficient and only slightly more useful. In this case you have to allocate the storage yourself and use special storage and retrieval functions. The details are straightforward and documented under Thread.AllocateDataSlot and associated methods.

Now it’s time to move on to a much bigger and more important topic – exclusion.

Exclusion

In 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 with other threads 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 favourite 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. Of course 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 as we will see.

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.

That is, 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 have 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 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.

<ASIN:B09FTLPTP9>



Last Updated ( Friday, 31 May 2024 )