Reliable way to prove that int++ is not atomic

Viewed 74

I can already see it's not by the incorrect increments, but there's just one small piece of the puzzle I can't quite seem to catch.

We have the following code:

internal class StupidObject
{
    static public SemaphoreSlim semaphore = new SemaphoreSlim(0, 100);

    private int counter;

    public bool MethodCall() => counter++ == 0;

    public int GetCounter() => counter;
}

And the following test code to try and see if it's an atomic operation:

var sharedObj = new StupidObject();
var resultTasks = new Task[100];
for (int i = 0; i < 100; i++)
{
    resultTasks[i] = Task.Run(async () =>
    {
        await StupidObject.semaphore.WaitAsync();
        if (sharedObj.MethodCall())
        {
            Console.WriteLine("True");
        };
    });
}
Console.WriteLine("Done");

Console.ReadLine();


StupidObject.semaphore.Release(100);

Console.ReadLine();

Console.WriteLine(sharedObj.GetCounter());

Console.ReadLine();

I expect to see multiple True's written to the console, but I ever see a single one.

Why is that? By my understanding, a ++ operation reads the value, increments the read value, and then stores that value to the variable.

Those are 3 operations. If we had a race condition, where thread A did the following:

  • Reads value to be 0.
  • Increments read value by 1.

And another thread B did the same things, but beat thread A to the third operation as following:

  • Writes read value to variable.

When A finishes writing the incremented read value, it should print back 0, same with thread B after it has done its write operation.

Am I missing something at the design aspect of things, or is my test not good enough to make this exact situation come to fruition?

Example without the Task Parallel Library (still yields a single True to the console):

var sharedObj = new StupidObject();
var resultTasks = new Thread[10000];
for (int i = 0; i < 10000; i++)
{
    resultTasks[i] = new Thread(() =>
    {
        StupidObject.semaphore.Wait();
        if (sharedObj.MethodCall())
        {
            Console.WriteLine("True");
        };
    });
    resultTasks[i].IsBackground = false;
    resultTasks[i].Start();
}
Console.WriteLine("Done");

Console.ReadLine();


StupidObject.semaphore.Release(10000);
1 Answers

What Liam said about Console.WriteLine is possible, but also there's another thing.

Starting Tasks doesn't equal starting threads, and even starting threads doesn't guarantee that all threads will begin immediatelly. Starting 100 short tasks probably won't even fill .Net's thread pool significantly, because those tasks end quickly and thread pool's manager probably won't start more than 3-5 threads. That's not the "immediate" and "parallel" you'd like to see when you want to start parallel 100 increments to race with each other, right? Remember that Tasks are queued first, then assigned to threads.

Note that the StupidObject's counter starts with zero and that's the ONLY MOMENT EVER that the value is zero. If ANY thread wins the race and successfully writes an update to that integer, you'll get FALSE in all future tasks, because it's already 1.

And if there are many tasks on the thread pool's queue, something first has to notice that fact. At program's start, thread pool lacks threads. They are not started in dozens right at program start. They are started on demand. Most probably you fill up the queue with 100 tasks, threadpool's thread is created, picks first task, bumps counter to 1, then maybe thread pool starts new threads to consume tasks faster.

To get a bit better image what's happening, instead of printing out 'true', collect values observed by return counter++: let each task run, finish, store its value in Task's .Result, then run threads/tasks, then wait for all of then to stop, then collect .Results and write a histogram of those values. Even if you don't see 5 zeros, maybe you will see 3 ones, 7 twos, 2 threes and so on.

Related