← Назад к вопросам

Потокобезопасный счётчик с множественными читателями

2.3 Middle🔥 121 комментариев
#Асинхронность и многопоточность

Условие

К серверу обращается множество параллельных клиентов:

  • Большинство клиентов читают текущее значение счётчика
  • Некоторые клиенты увеличивают значение счётчика

Требования:

  1. Читатели должны читать параллельно, без выстраивания в очередь по блокировке
  2. Запись должна быть эксклюзивной
  3. Реализовать потокобезопасный класс Counter
public class Counter
{
    private int _count = 0;
    
    public int Read() 
    {
        // TODO: реализовать
    }
    
    public void Increment() 
    {
        // TODO: реализовать
    }
}

Подсказка: Рассмотрите использование ReaderWriterLockSlim или Interlocked.

Критерии оценки:

  • Корректность многопоточной работы
  • Эффективность (читатели не блокируют друг друга)
  • Понимание выбранного механизма синхронизации

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

Анализ задачи

Это классическая задача на оптимизацию под сценарий read-heavy workload (множество читателей, редкие записи). Стандартная блокировка lock заставляла бы читателей выстраиваться в очередь, что неэффективно, так как они не конфликтуют друг с другом.

Выбор механизма: ReaderWriterLockSlim

ReaderWriterLockSlim — это специализированный примитив синхронизации, идеально подходящий для этой ситуации:

Преимущества:

  • Читатели не блокируют друг друга (параллельные читатели работают одновременно)
  • Запись получает эксклюзивный доступ
  • Лучше, чем обычная lock в сценариях с большим количеством читателей
  • Встроенный механизм справедливого распределения между читателями и писателями

Когда использовать:

  • Соотношение читателей к писателям >> 1 (например, 90% читает, 10% пишет)
  • Критична производительность при параллельном чтении

Реализация с ReaderWriterLockSlim

using System;
using System.Threading;

public class Counter
{
    private int _count = 0;
    private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
    
    public int Read()
    {
        _rwLock.EnterReadLock();
        try
        {
            return _count;
        }
        finally
        {
            _rwLock.ExitReadLock();
        }
    }
    
    public void Increment()
    {
        _rwLock.EnterWriteLock();
        try
        {
            _count++;
        }
        finally
        {
            _rwLock.ExitWriteLock();
        }
    }
}

Альтернатива: Interlocked для простых операций

Если нужна максимальная производительность и вы работаете только с простыми атомарными операциями, можно использовать Interlocked:

using System.Threading;

public class CounterInterlocked
{
    private int _count = 0;
    
    public int Read()
    {
        return Interlocked.Read(ref _count);
    }
    
    public void Increment()
    {
        Interlocked.Increment(ref _count);
    }
}

Преимущества Interlocked:

  • Нет явной блокировки (lock-free алгоритм)
  • Максимальная производительность
  • Не требует управления ресурсами

Недостатки:

  • Работает только с простыми типами (int, long, double, reference types)
  • Нельзя реализовать сложную логику (например, условное увеличение)

Сравнение подходов

ПодходИспользованиеПроизводительностьСложность
ReaderWriterLockSlim90% читает, 10% пишетОчень хорошо при большом числе читателейСредняя
InterlockedПростые атомарные операцииОтличная, lock-freeНизкая
lock (Monitor)Баланс чтения/записиХорошо для смешанных сценариевНизкая

Тестирование потокобезопасности

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

class Program
{
    static void Main()
    {
        var counter = new Counter();
        int numReaders = 50;
        int numWriters = 10;
        int iterations = 1000;

        // Запуск читателей
        var readerTasks = new Task[numReaders];
        for (int i = 0; i < numReaders; i++)
        {
            readerTasks[i] = Task.Run(() =>
            {
                for (int j = 0; j < iterations; j++)
                {
                    int value = counter.Read();
                    // Чтение значения
                }
            });
        }

        // Запуск писателей
        var writerTasks = new Task[numWriters];
        for (int i = 0; i < numWriters; i++)
        {
            writerTasks[i] = Task.Run(() =>
            {
                for (int j = 0; j < iterations; j++)
                {
                    counter.Increment();
                }
            });
        }

        Task.WaitAll(readerTasks);
        Task.WaitAll(writerTasks);

        Console.WriteLine($"Финальное значение счётчика: {counter.Read()}");
        Console.WriteLine($"Ожидаемое: {numWriters * iterations}");
    }
}

Продвинутая реализация: RecursiveReadLock

Для сценариев, где один поток может вложенно захватывать блокировку:

public class CounterRecursive
{
    private int _count = 0;
    private readonly ReaderWriterLockSlim _rwLock = 
        new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
    
    public int Read()
    {
        _rwLock.EnterReadLock();
        try
        {
            // Даже если в этом методе вызовем другой метод, требующий читателя,
            // не произойдёт deadlock
            return _count;
        }
        finally
        {
            _rwLock.ExitReadLock();
        }
    }
    
    public int ReadWithLogging()
    {
        _rwLock.EnterReadLock();
        try
        {
            Console.WriteLine($"Reading value: {Read()}");
            return _count;
        }
        finally
        {
            _rwLock.ExitReadLock();
        }
    }
}

Ключевые выводы

Для данной задачи рекомендуется:

  1. ReaderWriterLockSlim — идеален для read-heavy сценариев, явно показывает намерение
  2. Interlocked — если только простые атомарные операции и максимальная скорость
  3. lock — если соотношение чтения/записи близко к 1:1

Потокобезопасность гарантируется:

  • Читатели могут одновременно захватывать читательскую блокировку
  • Писатель получает эксклюзивный доступ
  • Нет race conditions благодаря синхронизированному доступу
  • Финальное значение счётчика всегда корректно