[C#] Ensuring Variable Visibility in Multi-Threaded Environments with the volatile Keyword

In multi-threaded programming, a problem can occur where a flag variable updated by one thread is not immediately “seen” (reflected) by another thread. This happens due to optimizations performed by the compiler or CPU, such as caching or instruction reordering.

By using the volatile keyword in C#, you can prevent optimizations that omit field access. This guarantees that the most up-to-date value is always read from and written to memory.

This article explains how to implement a flag variable to safely stop a background process from an external thread.

目次

Implementing a Stop Flag Using volatile

The following code simulates a server health check (heartbeat monitor). It uses a volatile field so that when the main thread issues a stop command, the background loop exits immediately.

Sample Code

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

public class Program
{
    public static async Task Main()
    {
        // Create an instance of the monitor class
        var monitor = new HeartbeatMonitor();

        Console.WriteLine("[Main] Starting monitor task...");

        // Run the monitoring loop on a separate thread
        Task monitorTask = Task.Run(() => monitor.StartMonitoring());

        // Wait a bit to ensure monitoring starts (simulation)
        Thread.Sleep(100);
        Console.WriteLine("[Main] Monitor task is running.");

        // Let it run for 2 seconds
        await Task.Delay(2000);

        Console.WriteLine("[Main] Sending stop request...");
        
        // Update the flag to request a stop
        monitor.RequestStop();

        // Wait for the task to finish
        await monitorTask;

        Console.WriteLine("[Main] Monitor task finished successfully.");
    }
}

public class HeartbeatMonitor
{
    // volatile declaration:
    // This indicates that this field may be accessed by multiple threads simultaneously.
    // This forces the compiler to read/write directly to main memory
    // instead of caching the variable.
    private volatile bool _shouldStop = false;

    public void StartMonitoring()
    {
        int count = 0;

        // Loop until _shouldStop becomes true.
        // Without volatile, Release build optimizations might assume
        // the value never changes, leading to an infinite loop risk.
        while (!_shouldStop)
        {
            count++;
            Console.WriteLine($"[Monitor] Sending heartbeat... ({count})");

            // Wait until next check
            // Even while the thread is sleeping, flag changes from other threads are valid
            Thread.Sleep(500);
        }

        Console.WriteLine("[Monitor] Stop flag detected. Exiting loop.");
    }

    /// <summary>
    /// Called from an external thread to set the stop flag
    /// </summary>
    public void RequestStop()
    {
        _shouldStop = true;
    }
}

Explanation and Technical Points

1. The Risk of “Infinite Loops” Due to Optimization

If the volatile keyword is missing from the _shouldStop variable, the compiler (especially in Release mode) or the JIT compiler might analyze the StartMonitoring method and assume that “_shouldStop is never modified inside this method.”

As a result, it may optimize the code by keeping the variable’s value in a register or cache and stop checking the value in main memory. If this happens, even if you call RequestStop() externally to change the value to true, the loop thread will not notice the change and will loop forever.

2. The Role of volatile (Guaranteeing Visibility)

Adding the volatile keyword to a field guarantees the following:

  • Memory Access Order: It suppresses instruction reordering by the compiler or CPU.
  • Reading the Latest Value: It forces the value to be read from or written to main memory, bypassing the CPU cache.

This ensures that a write performed by one thread is immediately “visible” to other threads.

3. Limitations and Notes

While volatile guarantees visibility, it does not guarantee atomicity.

For example, an increment operation like count++ involves three steps: read, add, and write. Even with volatile, if multiple threads write simultaneously, the value may become corrupted.

  • Use volatile for simple flags (ON/OFF) or variables where reading the latest value is the only requirement.
  • Use the lock statement or the Interlocked class if you need data integrity for calculations or exclusive control.
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

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

目次