Search code examples
c#multithreadingconcurrencythread-safetylockless

Lockless multithreading of an integer


Given a scenario where there's a function that should only be executed by one thread at any given time, and the rest just return (since a specific state is already being worked on), what's the best way to accomplish this?

public void RunOnce()
{
    if(Interlocked.Exchange(ref m_isRunning, 1) == 1)
        return;

    // Run code that should only be executed once
    
    // What mechanism do we use here to ensure thread safety?
    Volatile.Write(ref m_isRunning, 0);
}

Would the same mechanism apply if m_isRunning is a state (ie. an integer representing an enum)?


Solution

  • The code in your question is thread-safe IMHO, but in general the Interlocked.CompareExchange method is more flexible than the Interlocked.Exchange for implementing lock-free multithreading. Here is how I would prefer to code the RunOnce method:

    int _lock; // 0: not acquired, 1: acquired
    
    public void RunOnce()
    {
        bool lockTaken = Interlocked.CompareExchange(ref _lock, 1, 0) == 0;
        if (!lockTaken) return;
        try
        {
            // Run code that should be executed by one thread only.
        }
        finally
        {
            bool lockReleased = Interlocked.CompareExchange(ref _lock, 0, 1) == 1;
            if (!lockReleased)
                throw new InvalidOperationException("Could not release the lock.");
        }
    }
    

    My suggestion though would be to use the Monitor class:

    object _locker = new();
    
    public void RunOnce()
    {
        bool lockTaken = Monitor.TryEnter(_locker);
        if (!lockTaken) return;
        try
        {
            // Run code that should be executed by one thread only.
        }
        finally { Monitor.Exit(_locker); }
    }
    

    ...or the SemaphoreSlim class if you prefer to prevent reentrancy:

    SemaphoreSlim _semaphore = new(1, 1);
    
    public void RunOnce()
    {
        bool lockTaken = _semaphore.Wait(0);
        if (!lockTaken) return;
        try
        {
            // Run code that should be executed by one thread only.
        }
        finally { _semaphore.Release(); }
    }
    

    It makes the intentions of your code cleaner IMHO.