[C#] High-Speed Thread-Safe Implementation Using Atomic Operations with the Interlocked Class

When updating numbers in a multi-threaded environment, using simple addition operators (+= or ++) risks data inconsistency. This is because addition is split into three steps: “Read, Calculate, Write,” allowing interruptions between threads.

While using a lock statement solves this, it adds too much overhead (processing cost) for simple numeric updates. In such cases, using the System.Threading.Interlocked class, which provides CPU-level atomic (indivisible) operations, allows for fast and safe numeric updates.

Below, I explain an implementation example of safely updating a shared counter from multiple aggregation threads.

目次

Safe Addition Processing with Interlocked.Add

This code simulates aggregating access logs for a web server. Multiple threads report “access counts” simultaneously and update the value of a shared variable. By using Interlocked.Add, we achieve accurate aggregation without using locks.

Sample Code

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

public class Program
{
    public static void Main()
    {
        // 1. Create aggregation instance
        var counter = new AccessCounter();
        
        // Number of parallel processes (simulated concurrent connections)
        int taskCount = 5;

        Console.WriteLine($"Starting measurement (Parallel Tasks: {taskCount})...");

        // 2. Create and execute multiple tasks in parallel
        // Each task independently updates the counter
        var tasks = Enumerable.Range(0, taskCount)
            .Select(_ => Task.Run(() => counter.SimulateTraffic()))
            .ToArray();

        // Wait for all tasks to complete
        Task.WaitAll(tasks);

        // 3. Verify results
        Console.WriteLine("\n--- Results ---");
        Console.WriteLine($"Expected Total Hits: {taskCount * AccessCounter.BatchSize * AccessCounter.HitsPerBatch}");
        Console.WriteLine($"Actual Total Hits  : {AccessCounter.TotalHits}");
    }
}

public class AccessCounter
{
    // Static field shared between threads
    // Use the Interlocked class to update safely without locks
    public static int TotalHits = 0;

    // Simulation settings
    public const int BatchSize = 100;     // Loops per task
    public const int HitsPerBatch = 10;   // Number to add per update

    public void SimulateTraffic()
    {
        for (int i = 0; i < BatchSize; i++)
        {
            // Simulated processing time
            Thread.Sleep(5);

            // Add to shared resource
            RecordHits(HitsPerBatch);
        }
    }

    /// <summary>
    /// Safe addition to the counter
    /// </summary>
    private void RecordHits(int count)
    {
        // Interlocked.Add(ref variable, value)
        // Performs "Read-Add-Write" on TotalHits in memory in one go,
        // without interruption from other threads.
        
        // Using normal "TotalHits += count;" causes race conditions and incorrect values.
        Interlocked.Add(ref TotalHits, count);

        // For increasing by 1, Increment is also available
        // Interlocked.Increment(ref TotalHits);
    }
}

Explanation and Technical Points

1. The Danger of the “+=” Operator

The code TotalHits += 10; is broken down into three instructions for the CPU:

  1. Read the value of TotalHits from memory into a register.
  2. Add 10 to the value in the register.
  3. Write the calculation result back to TotalHits in memory.

In a multi-threaded environment, Thread B might read the old value before Thread A writes back, resulting in “Thread A’s addition being overwritten and lost.”

2. Atomic Operations

Methods in the Interlocked class (Add, Increment, Exchange, etc.) execute the three steps above as an atomic (indivisible) operation. Control is performed at the hardware level (like locking the memory bus), guaranteeing that no other thread can access that memory until the operation is complete.

3. Usage Distinction from lock

  • Interlocked: Specialized for simple integer incrementing, addition, and swapping. It is extremely fast.
  • lock: Used when you want to protect an entire block of code spanning multiple lines or maintain the integrity of complex objects. It is too costly for simple calculations.

4. The ref Keyword

Since Interlocked methods need to manipulate the “memory location (reference)” rather than the “value” of the variable, you must pass the argument with the ref keyword. This allows the method to directly rewrite the variable’s value internally.

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次