Потокобезопасный счётчик с множественными читателями
Условие
К серверу обращается множество параллельных клиентов:
- Большинство клиентов читают текущее значение счётчика
- Некоторые клиенты увеличивают значение счётчика
Требования:
- Читатели должны читать параллельно, без выстраивания в очередь по блокировке
- Запись должна быть эксклюзивной
- Реализовать потокобезопасный класс Counter
public class Counter
{
private int _count = 0;
public int Read()
{
// TODO: реализовать
}
public void Increment()
{
// TODO: реализовать
}
}
Подсказка: Рассмотрите использование ReaderWriterLockSlim или Interlocked.
Критерии оценки:
- Корректность многопоточной работы
- Эффективность (читатели не блокируют друг друга)
- Понимание выбранного механизма синхронизации
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Анализ задачи
Это классическая задача на оптимизацию под сценарий 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)
- Нельзя реализовать сложную логику (например, условное увеличение)
Сравнение подходов
| Подход | Использование | Производительность | Сложность |
|---|---|---|---|
| ReaderWriterLockSlim | 90% читает, 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();
}
}
}
Ключевые выводы
Для данной задачи рекомендуется:
- ReaderWriterLockSlim — идеален для read-heavy сценариев, явно показывает намерение
- Interlocked — если только простые атомарные операции и максимальная скорость
- lock — если соотношение чтения/записи близко к 1:1
Потокобезопасность гарантируется:
- Читатели могут одновременно захватывать читательскую блокировку
- Писатель получает эксклюзивный доступ
- Нет race conditions благодаря синхронизированному доступу
- Финальное значение счётчика всегда корректно