Mutexes in C#

In the previous example, we saw how to manage simple counter state using atomic operations. For more complex state, we can use a mutex to safely access data across multiple threads.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

// Container holds a dictionary of counters; since we want to
// update it concurrently from multiple threads, we
// add a lock object to synchronize access.
class Container
{
    private readonly object _lock = new object();
    private Dictionary<string, int> counters;

    public Container()
    {
        counters = new Dictionary<string, int> { { "a", 0 }, { "b", 0 } };
    }

    // Lock the object before accessing counters; unlock
    // it at the end of the method using a finally block.
    public void Inc(string name)
    {
        lock (_lock)
        {
            if (counters.ContainsKey(name))
                counters[name]++;
            else
                counters[name] = 1;
        }
    }

    public Dictionary<string, int> GetCounters()
    {
        lock (_lock)
        {
            return new Dictionary<string, int>(counters);
        }
    }
}

class Program
{
    static async Task Main()
    {
        var c = new Container();

        // This function increments a named counter
        // in a loop.
        async Task DoIncrement(string name, int n)
        {
            for (int i = 0; i < n; i++)
            {
                c.Inc(name);
            }
        }

        // Run several tasks concurrently; note
        // that they all access the same Container,
        // and two of them access the same counter.
        var tasks = new List<Task>
        {
            DoIncrement("a", 10000),
            DoIncrement("a", 10000),
            DoIncrement("b", 10000)
        };

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

        Console.WriteLine(string.Join(", ", c.GetCounters()));
    }
}

Running the program shows that the counters updated as expected.

$ dotnet run
[a, 20000], [b, 10000]

In this C# version, we’ve made the following adaptations:

  1. We use a Dictionary<string, int> instead of a map.
  2. The Mutex is replaced with C#’s lock keyword and a private object for synchronization.
  3. Instead of goroutines, we use C#’s Task-based asynchronous programming model.
  4. The WaitGroup is replaced by Task.WhenAll(), which waits for all tasks to complete.
  5. We’ve added a GetCounters() method to safely retrieve the current state of the counters.

This example demonstrates how to use locks in C# to safely access shared state from multiple threads, achieving the same goal as the original example.

Next, we’ll look at implementing this same state management task using only tasks and channels (which in C# would typically be implemented using producer-consumer patterns with collections like BlockingCollection<T> or channels from the System.Threading.Channels namespace).