Atomic Counters in C#

The primary mechanism for managing state in C# is through shared variables and synchronization primitives. Here we’ll look at using the System.Threading.Interlocked class for atomic counters accessed by multiple threads.

using System;
using System.Threading;
using System.Threading.Tasks;

class AtomicCounters
{
    static void Main()
    {
        // We'll use a long to represent our (always-positive) counter.
        long ops = 0;

        // Create a CountdownEvent to help us wait for all tasks to finish.
        var countdown = new CountdownEvent(50);

        // We'll start 50 tasks that each increment the counter exactly 1000 times.
        for (int i = 0; i < 50; i++)
        {
            Task.Run(() =>
            {
                for (int c = 0; c < 1000; c++)
                {
                    // To atomically increment the counter we use Interlocked.Increment.
                    Interlocked.Increment(ref ops);
                }

                countdown.Signal();
            });
        }

        // Wait until all the tasks are done.
        countdown.Wait();

        // Here no tasks are writing to 'ops', but it's safe to read the value
        // as all increments were done using Interlocked.Increment.
        Console.WriteLine($"ops: {ops}");
    }
}

We expect to get exactly 50,000 operations. Had we used a non-atomic integer and incremented it with ops++, we’d likely get a different number, changing between runs, because the tasks would interfere with each other. Moreover, we’d get data race failures when running with tools like the Thread Sanitizer.

To run the program:

$ dotnet run
ops: 50000

Next, we’ll look at lock statements, another tool for managing state in C#.