31

I'm creating two threads, and passing them a function which executes the code show below, 10,000,000 times.

Mostly, "5" is printed to the console. Sometimes it's "3" or "4". It's pretty clear why this is happening.

However, it's also printing "6". How is this possible?

class Program
{
    private static int _state = 3;

    static void Main(string[] args)
    {
        Thread firstThread = new Thread(Tr);
        Thread secondThread = new Thread(Tr);

        firstThread.Start();
        secondThread.Start();

        firstThread.Join();
        secondThread.Join();

        Console.ReadLine();
    }

    private static void Tr()
    {
        for (int i = 0; i < 10000000; i++)
        {
            if (_state == 3)
            {
                _state++;
                if (_state != 4)
                {
                    Console.Write(_state);
                }
                _state = 3;
            }
        }
    }
}

Here is the output:

Enter image description here

Rob
  • 26,989
  • 16
  • 82
  • 98
Zhambul
  • 942
  • 9
  • 23
  • I think the problem is that the field is not declared as volatile and the code is optimised for single threaded execution. Try with volatile – Oleg Sklyar Oct 25 '15 at 22:43
  • @Oleg it's still working with volatile – Zhambul Oct 25 '15 at 22:44
  • @Oleg Still happens with `volatile`, although it seems to happen less frequently. – xxbbcc Oct 25 '15 at 22:50
  • really nice question, i think the trick is on ++ operator, reading the value and holding it somewhere, then inccreasing it and setting again. – mehmet mecek Oct 25 '15 at 23:41
  • 6
    If you replace `_state++;` with `Interlocked.Increment(ref _state);` the problem goes away. A good reminder to use the `Interlocked` when updating shared variables in multi-threaded code. – Enigmativity Oct 25 '15 at 23:50
  • Voliatile only helps if one thread only reads and the other only writes. Why are we speculating about the manner in which concurrent code fails if it has no concurrency protection at all? There is no question here. This question should be closed. –  Oct 26 '15 at 14:11
  • Btw, is my understanding correct that this type (like any type) of race condition is simply undefined behavior in C++ and therefore "verboten"? – Peter - Reinstate Monica Oct 26 '15 at 16:01
  • @PeterSchneider: Yes. In C#, *slightly* more is guaranteed, namely memory-safety won't be subverted. (The respective C++ implementations also offer more guarantees than the standard...) – Deduplicator Oct 26 '15 at 17:55

2 Answers2

43

I think I have figured out the sequence of events leading to this issue:

Thread 1 enters if (_state == 3)

Context switch

Thread 2 enters if (_state == 3)
Thread 2 increments state (state = 4)

Context switch

Thread 1 reads _state as 4

Context switch

Thread 2 sets _state = 3
Thread 2 enters if (_state == 3)

Context switch

Thread 1 executes _state = 4 + 1

Context switch

Thread 2 reads _state as 5
Thread 2 executes _state = 5 + 1;

Rob
  • 26,989
  • 16
  • 82
  • 98
  • Wait a sec, is it likely to happen so that all the values that are output but one are shown as 5. I mean in general, this is an unlikely situation, how come he does not have 4 there at all, only 5s and one 6. – Oleg Sklyar Oct 25 '15 at 22:48
  • @Oleg It doesn't print `4` because of the `if`. Edit: Of course, I, too get `4` every once in a while. (Makes sense.) – xxbbcc Oct 25 '15 at 22:49
  • 3
    @Oleg Locally I do get `4`. I get 3, 4, 5 and 6 – Rob Oct 25 '15 at 22:50
  • 1
    @xxbbcc it does print 4 for me – Prix Oct 25 '15 at 22:51
  • @Prix Yes, I was too quick with my comment - me, too. – xxbbcc Oct 25 '15 at 22:52
  • 6
    "this is an unlikely situation" - 1 in a million chance... will happen very frequently in modern processors – WernerCD Oct 26 '15 at 00:14
  • 1
    The `4` gets printed because of another race condition - thread 1 sees `_state == 3`, so it drops into the if body, but in the meantime, thread 2 increments `_state`, so thread 1 prints out `4`. In an unsynchronized multi-threaded application, you really need to explore *all* the possible combinations of interleaving. There's a lot of those even in a program as simple as this :) – Luaan Oct 26 '15 at 08:09
  • 2
    There doesn't have to be a *'context switch'*, threads may run trully parallel on two cores. – CiaPan Oct 26 '15 at 08:37
  • 1
    Or perhaps there is a series of simultaneous events in which threads observe memory contents differently. There are race conditions that simply cannot be stated in this form, as a serialized stream of events. – Joker_vD Oct 26 '15 at 08:38
  • 1
    basically above code is not thread safe, due to which this is happening, am i right? @Rob – Ehsan Sajjad Oct 26 '15 at 14:45
16

This is a typical race condition. EDIT: In fact, there are multiple race conditions.

It can happen at any time where _state is 3 and both threads reach just past the if statement, either concurrently through context switching in a single core, or simultaneously in parallel in multiple cores.

This is because the ++ operator first reads _state and then increments it. It's possible that one got hold up enough time after the first if statement that it'll read 5 or even 6.

EDIT: If you'd generalize this example for N threads, you may observe a number as high as 3 + N+1.

This can be right when the threads start to run, or when one has just set _state to 3.

To avoid this, use a lock around the if statement, or use Interlocked to access _state, such as if (System.Threading.Interlocked.CompareAndExchange(ref _state, 3, 4) == 3) and System.Threading.Interlocked.Exchange(ref _state, 3).

If you want to keep the race condition, you should declare _state as volatile, otherwise you risk each thread seeing _state locally without updates from other threads.

In alternative, you may use System.Threading.Volatile.Read and System.Threading.Volatile.Write, in case you switch your implementation to have _state as a variable and Tr as a closure that captures that variable, as local variables can't be (and won't be able to be) declared volatile. In this case, even initialization must be done with a volatile write.


EDIT: Perhaps the race conditions are more apparent if we change the code slightly by expanding every read:

    // Without some sort of memory barrier (volatile, lock, Interlocked.*),
    // a thread is allowed to see _state as if other threads hadn't touched it
    private static volatile int _state = 3;

// ...

        for (int i = 0; i < 10000000; i++)
        {
            int currentState;
            currentState = _state;
            if (currentState == 3)
            {
                // RACE CONDITION: re-read the variable
                currentState = _state;
                currentState = currentState + 1:
                // RACE CONDITION: non-atomic read-modify-write
                _state = currentState;
                
                currentState = _state;
                if (currentState != 4)
                {
                    // RACE CONDITION: re-read the variable
                    currentState = _state;
                    Console.Write(currentState);
                }
                _state = 3;
            }
        }

I added comments in places where _state may be different than assumed by previous variable read statements.

Here's a long diagram, which shows it's even possible to print 6 twice in a row, once in each thread, like the image that the op posted. Remember, threads may not run in-synch, usually due to preemptive context-switching, cache stalls, or core speed differences (due to power saving or temporary turbo speed):

Race condition prints 6


This one is similar to the original, but it uses the Volatile class, where state is now a variable captured by a closure. The amount and order of volatile accesses becomes obvious:

    static void Main(string[] args)
    {
        int state = 3;

        ThreadStart tr = () =>
        {
            for (int i = 0; i < 10000000; i++)
            {
                if (Volatile.Read(ref state) == 3)
                {
                    Volatile.Write(ref state, Volatile.Read(state) + 1);
                    if (Volatile.Read(ref state) != 4)
                    {
                        Console.Write(Volatile.Read(ref state));
                    }
                    Volatile.Write(ref state, 3);
                }
            }
        };

        Thread firstThread = new Thread(tr);
        Thread secondThread = new Thread(tr);

        firstThread.Start();
        secondThread.Start();

        firstThread.Join();
        secondThread.Join();

        Console.ReadLine();
    }

Some thread-safe approaches:

    private static object _lockObject;

// ...

        // Do not allow concurrency, blocking
        for (int i = 0; i < 10000000; i++)
        {
            lock (_lockObject)
            {
                // original code
            }
        }

        // Do not allow concurrency, non-blocking
        for (int i = 0; i < 10000000; i++)
        {
            bool lockTaken = false;
            try
            {
                Monitor.TryEnter(_lockObject, ref lockTaken);
                if (lockTaken)
                {
                    // original code
                }
            }
            finally
            {
                if (lockTaken) Monitor.Exit(_lockObject);
            }
        }

        // Do not allow concurrency, non-blocking
        for (int i = 0; i < 10000000; i++)
        {
            // Only one thread at a time will succeed in exchanging the value
            try
            {
                int previousState = Interlocked.CompareExchange(ref _state, 4, 3);
                if (previousState == 3)
                {
                    // Allow race condition on purpose (for no reason)
                    int currentState = Interlocked.CompareExchange(ref _state, 0, 0);
                    if (currentState != 4)
                    {
                        // This branch is never taken
                        Console.Write(currentState);
                    }
                }
            }
            finally
            {
                Interlocked.CompareExchange(ref _state, 3, 4);
            }
        }

        // Allow concurrency
        for (int i = 0; i < 10000000; i++)
        {
            // All threads increment the value
            int currentState = Interlocked.Increment(ref _state);
            if (currentState == 4)
            {
                // But still, only one thread at a time enters this branch
                // Allow race condition on purpose (it may actually happen here)
                currentState = Interlocked.CompareExchange(ref _state, 0, 0);
                if (currentState != 4)
                {
                    // This branch might be taken with a maximum value of 3 + N
                    Console.Write(currentState);
                }
            }
            Interlocked.Decrement(ref _state);
        }


This one is a bit different, it takes the last known value of _state after the increment to perform something:

        // Allow concurrency
        for (int i = 0; i < 10000000; i++)
        {
            // All threads increment the value
            int currentState = Interlocked.Increment(ref _state);
            if (currentState != 4)
            {
                // Only the thread that incremented 3 will not take the branch
                // This can happen indefinitely after the first increment for N > 1
                // This branch might be taken with a maximum value of 3 + N
                Console.Write(currentState);
            }
            Interlocked.Decrement(ref _state);
        }

Note that the Interlocked.Increment/Interlocked.Decrement examples are not safe, unlike the lock/Monitor and Interlocked.CompareExchange examples, as there's no reliable way of knowing if the increment was successful or not.

One common approach is to increment, then follow with a try/finally where you decrement in the finally block. However, an asynchronous exception might be thrown (e.g. ThreadAbortException)

Asynchronous exceptions can be thrown in unexpected locations, possibly every machine instruction: ThreadAbortException, StackOverflowException, and OutOfMemoryException.

Another approach is to initialize currentState to something below 3 and conditionally decrement in a finally block. But again, in between Interlocked.Increment returning and currentState being assigned to the result, an asynchronous exception might occur, so currentState could still have the initial value even though the Interlocked.Increment succeeded.

acelent
  • 7,965
  • 21
  • 39
  • 4
    race condition only happens in code which is not thread safe right? – Ehsan Sajjad Oct 26 '15 at 14:50
  • 2
    @EhsanSajjad No, that's not true at all. Basically any program you ever write with multiple threads will have race conditions. Proper synchronization won't mean that only exactly one thing can happen, it just means that the program can do one of a bunch of different things *when you're okay with any of those things happening*. The bugs arise when there is a possible outcome that is undesirable, not when there's more than one. – Servy Oct 26 '15 at 15:29
  • 1
    @Servy did'nt understood completely your comment – Ehsan Sajjad Oct 26 '15 at 16:46
  • 1
    @Airzooka No, Servy was saying that multi-threaded situations are *non-deterministic*, not that they are difficult, unpredictable, improper, wrong, malformed, etc. **But you have to actually put some concurrency control in**, or it is nonsense (that's a technical term). The code for this Question is Nonsense. –  Oct 26 '15 at 17:18