Какие способы безопасной блокировки (lock) при асинхронных операциях в C# ты знаешь?

«Какие способы безопасной блокировки (lock) при асинхронных операциях в C# ты знаешь?» — вопрос из категории Многопоточность, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

При работе с асинхронным кодом обычный lock не подходит, так как он блокирует поток. Для безопасной синхронизации в async/await контексте используются специальные примитивы.

Основные способы:

  1. SemaphoreSlim — наиболее распространённый и гибкий вариант для ограничения доступа к ресурсу или секции кода. Поддерживает асинхронное ожидание.

    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); // Одновременно один поток
    
    public async Task AccessResourceAsync()
    {
        await _semaphore.WaitAsync();
        try
        {
            // Критическая секция, работа с общим ресурсом
            await SomeAsyncOperation();
        }
        finally
        {
            _semaphore.Release();
        }
    }
  2. AsyncLock (из библиотеки Nito.AsyncEx) — предоставляет более идиоматичный для C# синтаксис, похожий на lock, но для асинхронных методов.

    private readonly AsyncLock _mutex = new AsyncLock();
    
    public async Task AccessResourceAsync()
    {
        using (await _mutex.LockAsync())
        {
            // Критическая секция
            await SomeAsyncOperation();
        }
    }
  3. Channel или BufferBlock<T> (из TPL Dataflow) — для сценариев, где синхронизацию можно заменить очередью сообщений (producer/consumer). Это часто более масштабируемый и чистый подход.

    private readonly Channel<MyMessage> _channel = Channel.CreateUnbounded<MyMessage>();
    
    // Producer
    public async Task ProduceAsync(MyMessage msg) => await _channel.Writer.WriteAsync(msg);
    
    // Consumer (запускается один раз)
    public async Task ConsumeAsync(CancellationToken ct)
    {
        await foreach (var msg in _channel.Reader.ReadAllAsync(ct))
        {
            // Обработка сообщения. Конкуренции нет, так как читает один потребитель.
        }
    }
  4. Иммутабельные структуры данных и Interlocked — для простых операций (инкремент, сравнение с обменом) используйте атомарные операции класса Interlocked. Лучший "лок" — это его отсутствие: проектируйте систему так, чтобы общее состояние было неизменяемым или использовалось без блокировок.

Ключевой принцип: Избегайте блокировки потоков в асинхронном коде. Всегда предпочитайте асинхронные примитивы (WaitAsync) синхронным (Wait).