Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# Threading: a race condition example

I am reading http://www.mono-project.com/ThreadsBeginnersGuide.

The first example looks like this:

public class FirstUnsyncThreads {
    private int i = 0;

    public static void Main (string[] args) {
        FirstUnsyncThreads myThreads = new FirstUnsyncThreads ();
    }

    public FirstUnsyncThreads () {
        // Creating our two threads. The ThreadStart delegate is points to
        // the method being run in a new thread.
        Thread firstRunner = new Thread (new ThreadStart (this.firstRun));
        Thread secondRunner = new Thread (new ThreadStart (this.secondRun));

        // Starting our two threads. Thread.Sleep(10) gives the first Thread
        // 10 miliseconds more time.
        firstRunner.Start ();
        Thread.Sleep (10);
        secondRunner.Start ();
    }

    // This method is being excecuted on the first thread.
    public void firstRun () {
        while(this.i < 10) {
            Console.WriteLine ("First runner incrementing i from " + this.i +
                              " to " + ++this.i);
            // This avoids that the first runner does all the work before
            // the second one has even started. (Happens on high performance
            // machines sometimes.)
            Thread.Sleep (100);
        }
    }

    // This method is being excecuted on the second thread.
    public void secondRun () {
        while(this.i < 10) {
            Console.WriteLine ("Second runner incrementing i from " + this.i +
                              " to " + ++this.i);
            Thread.Sleep (100);
        }
    }
}

Output:

First runner incrementing i from 0 to 1
Second runner incrementing i from 1 to 2
Second runner incrementing i from 3 to 4
First runner incrementing i from 2 to 3
Second runner incrementing i from 5 to 6
First runner incrementing i from 4 to 5
First runner incrementing i from 6 to 7
Second runner incrementing i from 7 to 8
Second runner incrementing i from 9 to 10
First runner incrementing i from 8 to 9

Wow, what is this? Unfortunately, the explanation in the article is inadequate for me. Can you explain me why the increments happened in a jumbled order?

Thanks!

like image 657
George Avatar asked Aug 22 '09 12:08

George


3 Answers

I think the writer of the article has confused things.

VoteyDisciple is correct that ++i is not atomic and a race condition can occur if the target is not locked during the operation but this will not cause the issue described above.

If a race condition occurs calling ++i then internal operations of the ++ operator will look something like:-

  1. 1st thread reads value 0
  2. 2nd thread reads value 0
  3. 1st thread increments value to 1
  4. 2nd thread increments value to 1
  5. 1st thread writes value 1
  6. 2nd thread writes value 1

The order of operations 3 to 6 is unimportant, the point is that both the read operations, 1 and 2, can occur when the variable has value x resulting in the same incrementation to y, rather than each thread performing incrementations for distinct values of x and y.

This may result in the following output:-

First runner incrementing i from 0 to 1
Second runner incrementing i from 0 to 1

What would be even worse is the following:-

  1. 1st thread reads value 0
  2. 2nd thread reads value 0
  3. 2nd thread increments value to 1
  4. 2nd thread writes value 1
  5. 2nd thread reads value 1
  6. 2nd thread increments value to 2
  7. 2nd thread writes value 2
  8. 1st thread increments value to 1
  9. 1st thread writes value 1
  10. 2nd thread reads value 1
  11. 2nd thread increments value to 2
  12. 2nd thread writes value 2

This may result in the following output:-

First runner incrementing i from 0 to 1
Second runner incrementing i from 0 to 1
Second runner incrementing i from 1 to 2
Second runner incrementing i from 1 to 2

And so on.

Furthermore, there is a possible race condition between reading i and performing ++i since the Console.WriteLine call concatenates i and ++i. This may result in output like:-

First runner incrementing i from 0 to 1
Second runner incrementing i from 1 to 3
First runner incrementing i from 1 to 2

The jumbled console output which the writer has described can only result from the unpredictability of the console output and has nothing to do with a race condition on the i variable. Taking a lock on i whilst performing ++i or whilst concatenating i and ++i will not change this behaviour.

like image 184
Adam Ralph Avatar answered Oct 17 '22 14:10

Adam Ralph


When I run this (on a dualcore), my output is

First runner incrementing i from 0 to 1
Second runner incrementing i from 1 to 2
First runner incrementing i from 2 to 3
Second runner incrementing i from 3 to 4
First runner incrementing i from 4 to 5
Second runner incrementing i from 5 to 6
First runner incrementing i from 6 to 7
Second runner incrementing i from 7 to 8
First runner incrementing i from 8 to 9
Second runner incrementing i from 9 to 10

As I would have expected. You are running two loops, both executing Sleep(100). That is very ill suited to demonstrate a race-condition.

The code does have a race condition (as VoteyDisciple describes) but it is very unlikely to surface.

I can't explain the lack of order in your output (is it a real output?), but the Console class will synchronize output calls.

If you leave out the Sleep() calls and run the loops 1000 times (instead of 10) you might see two runners both incrementing from 554 to 555 or something.

like image 34
Henk Holterman Avatar answered Oct 17 '22 13:10

Henk Holterman


Synchronization is essential when multiple threads are present. In this case you are seeing that both threads read and write to this.i , but no good attempt is done at synchronize these accesses. Since both of them concurrently modify the same memory area, you observe the jumbled output. The call to Sleep is dangerous, it is an approach which leads to sure bugs. You cannot assume that the threads will be always displaced by the inital 10 ms.

In short: Never use Sleep for synchronization :-) but instead adopt some kind of thread synchronization technique (eg. locks, mutexes, semaphores). Always try to use the lightest possible lock that will fulfill your need....

A useful resource is the book by Joe Duffy, Concurrent Programming on Windows.

like image 42
Francesco Avatar answered Oct 17 '22 14:10

Francesco