Bak2Basics: Threading: The problem of re-entrant code

Posted on 10/20/2006 @ 6:28 AM in #Bak2Basics by | Feedback | 55483 views

Code reentrancy is probably the #1 issue you will have to worry about when writing multithreaded applications.

When you write a piece of code, your code turns into a number of instructions (duH!), and those instructions work with data (duH!).

Instructions are sequential – it doesn’t matter who is executing them, their behavior will always be consistent. But where they screw up, is the data that they are written to manage. There are two kinds of data for a piece of code,

- Instance data, which every callable instance creates afresh. Thus in the following code, “i” is the instance data.

public void SomeMethod()
{
   
int i ;
}

The above is declared on stack – there could be other instance data that is declared on the heap (speaking of which, value type, ref type, stack, heap – another interesting blog post possible eh?)

- Shared data, which every callable instance shares an instance of. Thus in the following code, “I” is the shared data.

static int i;
public
void SomeMethod()
{
   
// ...

}

It is this shared data, that tends to get corrupted very easily in multi-threaded environments. Why? Well because there are multiple sets of instructions that are working concurrently on the shared “i”.

In other words, the problem of a thread execution entering a set of instructions, while another thread is still executing those set of instructions – thus possibly corrupting shared data, is referred to as the problem of the problem of reentrancy. And code, that can be used safely from multiple threads or processes is referred to as reentrant code.

Let us see an example. Consider this rather simple piece of code:

class MyClass
{
   
static int i = 0;

    public void SomeMethod()
   
{
       
i++;
       
Thread.Sleep(1);
       
Console.WriteLine(i);
   
}
}

 

Now let me go ahead and fire up 10 threads.  What I am hoping to achieve here is, digits printed out from 1 thru 10. They can be out of order since I haven’t forced an order of executing between these threads using Thread.Join (See: Waiting for another thread to finish), but they shouldn’t miss any digits.

Here is the code for firing up 10 threads for the above method.

Thread[] threads = new Thread[10];
MyClass
classInstance = new MyClass();
 

for
(int i = 0; i < 10; i++)
{
   
threads[i] = new Thread(classInstance.SomeMethod);
   
threads[i].Start();
}

 

When I run the above, here is the output produced (the exact output may differ on your machine):

3 3 3 5 5 7 7 8 10 10

And when you run this piece of code, it seems to behave like your female counterpart – never predictable results.

What is happening here is that, before a thread is done doing its work – another thread comes in and corrupts it’s data. So essentially, I need a way to specify that, “While I am working on this piece of code, make sure that nobody else screws with it”. In other words, “Only one thread can work on this piece of code at a time”.

Well that’s easy. Just modify SomeMethod to this –

public void SomeMethod()
{
   
lock (this)
   
{
       
i++;
       
Thread.Sleep(1);
       
Console.Write(i + " ");
   
}
}

Yep !! That’s it. The lock keyword (SyncLock for my verbose VB friends) is what will force only one thread to mess with that code portion at a time. Of course, the more stuff you lock, the more other threads wait – and the more you tend to introduce deadlocks. The trick is to lock as little as possible, for as little time as possible.

Here are a few other salient points -

For a simpler implementation in the case of simple add/subtract etc., you could use the Interlocked class.

For a finer level control you could use the Monitor class to Monitor a particular variable, request a lock on a variable with a timeout specified etc.

For a better concurrent performance, you could use the ReaderWriterLock class to lock resources for reading or writing. Thus multiple folks can read at the same time – but for a writer to acquire a lock, nobody must be reading it at that time.

For a better concurrent performance with corrupt data, you can use VolatileRead or VolatileWrite (or the volatile keyword).

Sound off but keep it civil:

Older comments..


On 3/29/2012 4:18:27 AM Tobin Titus said ..
And for the C++ version:


http://tobint.com/blog/unsafe-thread-safety/

:)