Stateful Goroutines in C#

Our example demonstrates the use of stateful goroutines in C#. Instead of goroutines, we’ll use Tasks and channels will be replaced with BlockingCollections.

using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    // Define read and write operation classes
    class ReadOp
    {
        public int Key { get; set; }
        public BlockingCollection<int> Resp { get; set; }
    }

    class WriteOp
    {
        public int Key { get; set; }
        public int Val { get; set; }
        public BlockingCollection<bool> Resp { get; set; }
    }

    static void Main()
    {
        // Initialize operation counters
        long readOps = 0;
        long writeOps = 0;

        // Create channels for read and write operations
        var reads = new BlockingCollection<ReadOp>();
        var writes = new BlockingCollection<WriteOp>();

        // Start the state-owning task
        Task.Run(() =>
        {
            var state = new ConcurrentDictionary<int, int>();
            while (true)
            {
                if (reads.TryTake(out ReadOp read, 100))
                {
                    read.Resp.Add(state.GetOrAdd(read.Key, 0));
                }
                else if (writes.TryTake(out WriteOp write, 100))
                {
                    state[write.Key] = write.Val;
                    write.Resp.Add(true);
                }
            }
        });

        // Start 100 read tasks
        for (int r = 0; r < 100; r++)
        {
            Task.Run(() =>
            {
                var random = new Random();
                while (true)
                {
                    var read = new ReadOp
                    {
                        Key = random.Next(5),
                        Resp = new BlockingCollection<int>()
                    };
                    reads.Add(read);
                    read.Resp.Take();
                    Interlocked.Increment(ref readOps);
                    Thread.Sleep(1);
                }
            });
        }

        // Start 10 write tasks
        for (int w = 0; w < 10; w++)
        {
            Task.Run(() =>
            {
                var random = new Random();
                while (true)
                {
                    var write = new WriteOp
                    {
                        Key = random.Next(5),
                        Val = random.Next(100),
                        Resp = new BlockingCollection<bool>()
                    };
                    writes.Add(write);
                    write.Resp.Take();
                    Interlocked.Increment(ref writeOps);
                    Thread.Sleep(1);
                }
            });
        }

        // Let the tasks work for a second
        Thread.Sleep(1000);

        // Report the operation counts
        Console.WriteLine($"readOps: {readOps}");
        Console.WriteLine($"writeOps: {writeOps}");
    }
}

This C# program demonstrates state management using Tasks and BlockingCollections, which are analogous to goroutines and channels in Go.

We define ReadOp and WriteOp classes to encapsulate read and write requests. The main program creates BlockingCollections for reads and writes, which act as channels for communication between tasks.

A state-owning task is created to manage a private dictionary. It continuously processes read and write requests from the respective collections.

We then start 100 read tasks and 10 write tasks. Each task repeatedly creates operation objects, sends them through the appropriate collection, and waits for a response.

After letting the tasks run for a second, we report the total number of read and write operations performed.

To run the program, compile and execute it:

$ dotnet run
readOps: 71708
writeOps: 7177

The output shows that this task-based state management example completes about 80,000 total operations in one second, similar to the goroutine-based approach in Go.

While this approach might be more complex than using locks, it can be beneficial in scenarios involving multiple channels or when managing multiple locks would be error-prone. Choose the approach that feels most natural and helps ensure the correctness of your program.