Search code examples
c#.net-6.0.net-8.0

.NET 8 causing CPU overload when using ReaderWriterLockSlim


I migrated a project from .NET 6 to .NET 8 and encountered an issue with CPU performance when using ReaderWriterLockSlim. With .NET 8, CPU consumption is way higher (4-5x higher) than .NET 6. I have used below code sample to test the CPU performance:

The code sample used for testing was generated using ChatGPT:

public class ReaderWriterSlimTests
{
    private static readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();

    private static int _sharedResource = 0;
    private static readonly int ReaderCount = Environment.ProcessorCount * 2;
    private static readonly int WriterCount = Environment.ProcessorCount / 2;
    private static readonly int Iterations = 1_000_000;
    private static volatile bool _running = true;

    public void RunTests()
    {
        Console.WriteLine($"Running test on .NET {Environment.Version}");
        Console.WriteLine($"Readers: {ReaderCount}, Writers: {WriterCount}, Iterations: {Iterations}");

        Stopwatch stopwatch = Stopwatch.StartNew();
        Process process = Process.GetCurrentProcess();
        TimeSpan startCpuTime = process.TotalProcessorTime;

        Thread[] readers = new Thread[ReaderCount];
        Thread[] writers = new Thread[WriterCount];

        // Start readers
        for (int i = 0; i < ReaderCount; i++)
        {
            readers[i] = new Thread(ReaderTask);
            readers[i].Start();
        }

        // Start writers
        for (int i = 0; i < WriterCount; i++)
        {
            writers[i] = new Thread(WriterTask);
            writers[i].Start();
        }

        // Run for a few seconds
        Thread.Sleep(30000);
        _running = false;

        // Wait for all threads to finish
        foreach (var reader in readers) reader.Join();
        foreach (var writer in writers) writer.Join();

        stopwatch.Stop();
        TimeSpan endCpuTime = process.TotalProcessorTime;
        double cpuUsage = (endCpuTime - startCpuTime).TotalMilliseconds / stopwatch.ElapsedMilliseconds * 100;

        Console.WriteLine($"Elapsed Time: {stopwatch.ElapsedMilliseconds} ms");
        Console.WriteLine($"CPU Usage Approximation: {cpuUsage:F2}%");
    }

    private void ReaderTask()
    {
        while (_running)
        {
            _lock.EnterReadLock();
            var value = _sharedResource; // Simulate read
            _lock.ExitReadLock();
        }
    }

    private void WriterTask()
    {
        Random rnd = new Random();
        while (_running)
        {
            _lock.EnterWriteLock();
            _sharedResource = rnd.Next(); // Simulate write
            _lock.ExitWriteLock();
        }
    }
}

If I run this code in .NET 6, CPU usage is around 300%. Same code in .NET 8 is around 1800%.

Any idea what has caused this performance degradation in .NET 8? Also, going forward, how to fix this issue in .NET 8?

Thanks Vik


Solution

  • There was no performance degradation, quite the opposite - performance improved in .NET 7 by fixing a bug that caused long latencies in .NET 6. The ChatGPT code doesn't measure the actual execution time, so the average CPU usage isn't useful.

    ReaderWriterLockSlim is meant to peg a core at 100%, because it uses spinwaiting instead of putting a thread to sleep and thus causing it to get evicted from a core. Evicting a thread and rescheduling is an expensive operation in any OS and there's no way to know when the OS will reschedule a thread for execution. The scheduler on Windows runs roughly every 15ms, so the blocked thread may have to wait that much or more to get rescheduled.

    All Slim versions of any concurrency objects use spinwaiting to avoid getting the thread evicted. All of them are meant for scenarios where the delay is expected to be small, definitely smaller than the 15ms of a scheduling quant.

    There was a bug in .NET 6, which caused ReaderWriterLockSlim to not spin enough, leading to long delays - a Thread.Sleep(0)/Thread.Sleep(1) caused the thread to get evicted : There may be unexpectedly high delays in ReaderWriterLockSlim operations. The bug's reproduction code shows a huge delay in tight loops - 15-45ms delays which suggest that 1-2 rescheduling chances were missed.

    That was fixed in .NET 7 by replacing Thread.Sleep with adaptive code that keeps using Thread.Sleep() in single-core machines, or a spinwait with exponential backoff in multi-core machines.

    In short, there's no bug to fix and no plans to reintroduce the .NET 6 bug.