История небольшого бага с использованием SemaphoreSlim в C#
Где-то на сервере жил-был код:
try
{
await semaphoreSlim.WaitAsync(cancellationToken);
await DoSomething();
}
finally
{
semaphoreSlim.Release();
}
Ничего интересного: ожидаем семафор, делаем какую-то работу и по завершении освобождаем в Release
.
Всё работало нормально, но в какой-то момент стали проскакивать исключенияSemaphoreFullException
. Чтобы понять, когда они возникают, нужно вспомнить, как работает SemaphoreSlim.
SemaphoreSlim - имеет 2 основных параметра: текущее значение и максимальное. WaitAsync
- уменьшает текущее значение на один, Release
- увеличивает. Если вызывать WaitAsync
, когда текущее значение равно 0, то нужно дождаться вызова Release
. Если вызывать Release
, когда текущее значение равно максимальному, то будет выброшено исключение SemaphoreFullException.
Получается, что Release
вызывается больше раз чем WaitAsync
. Как такое может быть? По коду - никак. Но, по факту, важен не сам вызов WaitAsync
, а изменение текущего значения счётчика.
Всё дело в cancellationToken
. Если запрошена отмена операции, то WaitAsync бросает исключение до изменения текущего значения. Далее в блоке finally
исходное исключение перекрывается другим исключением, и мы теряем исходную ошибку.
Т.е. правильный код должен выглядеть вот так:
await semaphoreSlim.WaitAsync(cancellationToken);
try
{
await DoSomething();
}
finally
{
semaphoreSlim.Release();
}
И невероятный фикс с перемещением строчки уезжает на тестирование.