Асинхронное ожидание внутри lock-а C#

Posted by PDSW on Wednesday, October 31, 2018
Last Modified on Monday, February 11, 2019

TOC

Вы когда-нибудь пытались ожидать завершение выполнения задачи внутри блока lock на C#? Если Вам приходила в голову такая идея но никак не доходили руки до воплощения, то предлагаю не спешить. Во-первых компилятор не даст это сделать, а во-вторых у разработчиков dotnet есть другие возможности решать проблему блокировки раделяемых ресурсов в асинхронном коде. Предлагаю с ними ознакомится под катом.

От переводчика: открыл для себя классный блог Christopher Demicoli и сразу же делюсь свеженьким переводом

Вы когда-нибудь пытались ожидать выполнение задачи внутри блока lock? На языке C# такое выражение невалидно:

lock (lockObject)
{
    await Task.Delay(1000);
}

Ключевое слово lock можно использовать только для синхронизации синхронного кода. Из MSDN:

Выражение await не может объявляться в теле синхронной функции, в query expression, в блоке оператора блокировки или в unsafe контексте.

Начиная с введения C# 5, async / await используется почти везде. И почему бы нет? Компилятор выполняет сложную работу, которую ранее выполнял разработчик, а приложение сохраняет логическую структуру, которая напоминает синхронный код. В результате вы получаете все преимущества асинхронного программирования при небольших усилиях.

Проблема заключается как-раз в том, что необходимость синхронизации асинхронных блоков кода возникает довольно часто. Эрик Липперт отмечает, что причина, по которой это не реализована командой компилятора, заключается не в том, что ее сложно реализовать, а в том, чтобы защитить разработчика от ошибок; Ожидание внутри lock-а - это рецепт создания deadlock-ов.

Введение в Мьютексы и Семафоры

Говоря простыми словами, семафор - это тип данных, который используется для синхронизации доступа из нескольких потоков. Семафоры являются полезным инструментом в предотвращении состояния гонки. Существует два типа семафоров:

  1. Семафор счётчик. Как следует из названия, семафор считает скольким одновременным потокам он позволил обратится к общему ресурсу (до максимального количества, которое вы указываете). Когда потоки запрашивают доступ к ресурсу, счетчик семафора уменьшается, а когда они освобождают его, он снова увеличивается.
  2. Двоичные семафоры или Мьютексы: Мьютекс - это, по сути, семафор со значением 1. Мьютекс не позволит пройти нескольким потокам одновременно; он обеспечивает взаимное исключение (на агл. - “mutual exclusion” отсюда и название). Пока кто-то владеет мьютексом, остальные должны ждать.

.NET Semaphore и SemaphoreSlim

System.Threading.Semaphore - это оболочка вокруг объекта семафора Win32 (семафор счётчик). Это системный семафор, поэтому он может использоваться между несколькими процессами.

С другой стороны, System.Threading.SemaphoreSlim - это легкий, быстрый семафор, который предоставляется CLR и используется для ожидания в рамках одного процесса, когда предположительное время ожидания будет очень коротким.

Замена Lock-а семафором

Итак, теперь, когда мы знаем, что такое Семафор, мы можем ворваться в код и заменить lock семафором. В нашем случае класс SemaphoreSlim является идеальным типом данных, поскольку мы будем использовать его всегда в одном процессе.

Крайне важно всегда освобождать Семафор, когда вы выолнили свою задачу, поэтому рекомендуется помещать его внутри tryfinally выражения.

Вызов WaitAsync на семафоре создает задачу, которая будет завершена, когда этому потоку будет предоставлен доступ к семафору.

// Создаем экземпляр Singleton семафора со значением 1. 

// Это означает, что только один поток может получить доступ за раз.

static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1,1);

// Асинхронно ждём входа в семафор. 

// Если кому-то не предоставлен доступ к Семафору, 

// выполнение кода будет продолжено, иначе этот поток ждет здесь до тех пор, 

// пока семафор не будет выпущен 

await SemaphoreSlim.WaitAsync();
try
{
    await Task.Delay(1000);
}
finally
{
    // Крайне важно ВСЕГДА освобождать семафор, 
    
    // иначе мы получим семафор, который навсегда будет заблокирован.
    
    // Вот почему важно сделать Release в предложении try ... finally; 
    
    // Выполнение программы может привести к сбою или перейти по другому пути, 
    
    // таким образом вам гарантируется дальнейшее выполнение программы 

    semaphoreSlim.Release();
}

comments powered by Disqus