I have written the follwing code to demonstrate race condition between 2 threads of a same process.
`
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
int c = 0;
void *fnC()
{
int i;
for(i=0;i<10;i++)
{
c++;
printf(" %d", c);
}
}
int main()
{
int rt1, rt2;
pthread_t t1, t2;
/* Create two threads */
if( (rt1=pthread_create( &t1, NULL, &fnC, NULL)) )
printf("Thread creation failed: %d\n", rt1);
if( (rt2=pthread_create( &t2, NULL, &fnC, NULL)) )
printf("Thread creation failed: %d\n", rt2);
/* Wait for both threads to finish */
pthread_join( t1, NULL);
pthread_join( t2, NULL);
printf ("\n");
return 0;
}
`
I ran this program, and expected a race condition to occur between the 2 threads (but, I understand that the probablity for race condition is very less, as the thread main function very small).
I ran this 50000 times. The following was the output,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 - 49657 times (no race condition)
1 3 4 5 6 7 8 9 10 11 2 12 13 14 15 16 17 18 19 20 - 244 times (race condition occurs)
2 3 4 5 6 7 8 9 10 11 1 12 13 14 15 16 17 18 19 20 - 99 times (race condition occurs)
The question is,
When race condition occurs as in output 2, thread 1 prints 1 and is swapped out of the processor and thread 2 comes in. It starts working and after thread 2 prints 11, it gets swapped out, thread 1 comes in. It has to print 12, but rather it is printing 2 (actually 2 should be missing). I can’t figure out how. Please help me understand what happens here.
You’re thinking in a C mindset, but if you want to think about race conditions you have to think on a lower level.
In a debugger, you normally set a breakpoint on a single line of code, and you can watch each line of code being executed by stepping through your program. But that’s not how the machine works, the machine may execute several instructions for each line of code and threads can be interrupted anywhere.
Let’s look at this one line.
In machine code, it looks something like this:
So the behavior is not unexpected. You have to load the value of
cbefore you can callprintf, so if the thread is interrupted there is always a chance thatcis stale by the timeprintfdoes anything. Unless you do something to stop it.Fixing the race condition:
What does
volatiledo?The code in the question can translate to assembly code like the following:
It doesn’t have to reload
cafter it increments it, since the C compiler is allowed to assume that nobody else (no other thread or device) changes memory except the current thread.If you use
volatile, the compiler will be strict about keeping every load and store operation, and the assembly will look something like this:This doesn’t help. In fact,
volatilealmost never helps. Most C programmers don’t understandvolatile, and it’s almost useless for writing multithreaded code. It’s useful for writing signal handlers, memory-mapped IO (device drivers / embedded programming), and it’s useful for correctly usingsetjmp/longjmp.Footnote:
The compiler can’t cache the value of
cacross a call toprintf, because as far as the compiler knows,printfcan changec(cis a global variable, after all). Someday, the compiler may get more sophisticated and it may know thatprintfdoesn’t changec, so the program may break even more severly.