Ответ
При работе с асинхронным кодом обычный lock не подходит, так как он блокирует поток. Для безопасной синхронизации в async/await контексте используются специальные примитивы.
Основные способы:
-
SemaphoreSlim— наиболее распространённый и гибкий вариант для ограничения доступа к ресурсу или секции кода. Поддерживает асинхронное ожидание.private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); // Одновременно один поток public async Task AccessResourceAsync() { await _semaphore.WaitAsync(); try { // Критическая секция, работа с общим ресурсом await SomeAsyncOperation(); } finally { _semaphore.Release(); } } -
AsyncLock(из библиотекиNito.AsyncEx) — предоставляет более идиоматичный для C# синтаксис, похожий наlock, но для асинхронных методов.private readonly AsyncLock _mutex = new AsyncLock(); public async Task AccessResourceAsync() { using (await _mutex.LockAsync()) { // Критическая секция await SomeAsyncOperation(); } } -
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)) { // Обработка сообщения. Конкуренции нет, так как читает один потребитель. } } -
Иммутабельные структуры данных и
Interlocked— для простых операций (инкремент, сравнение с обменом) используйте атомарные операции классаInterlocked. Лучший "лок" — это его отсутствие: проектируйте систему так, чтобы общее состояние было неизменяемым или использовалось без блокировок.
Ключевой принцип: Избегайте блокировки потоков в асинхронном коде. Всегда предпочитайте асинхронные примитивы (WaitAsync) синхронным (Wait).