Отмена SemaphoreSlim.WaitAsync с сохранением блокировки семафора

В одном из наших занятий мы активно используем SemaphoreSlim.WaitAsync(CancellationToken) и отмена этого.

Я, кажется, столкнулся с проблемой, когда ожидающий вызов WaitAsync отменяется вскоре после звонка SemaphoreSlim.Release() (вкратце, я имею в виду до ThreadPool имел возможность обработать элемент из очереди), он переводит семафор в состояние, когда дальнейшие блокировки не могут быть получены.

Из-за недетерминированной природы ThreadPool элемент выполняется между вызовом Release() а также Cancel(), следующий пример не всегда демонстрирует проблему, для тех обстоятельств, которые я прямо сказал, игнорировать этот прогон.

Это мой пример, который пытается продемонстрировать проблему:

void Main()
{
    for(var i = 0; i < 100000; ++i)
        Task.Run(new Func<Task>(SemaphoreSlimWaitAsyncCancellationBug)).Wait();
}

private static async Task SemaphoreSlimWaitAsyncCancellationBug()
{
    // Only allow one thread at a time
    using (var semaphore = new SemaphoreSlim(1, 1))
    {
        // Block any waits
        semaphore.Wait();

        using(var cts1 = new CancellationTokenSource())
        {
            var wait2 = semaphore.WaitAsync(cts1.Token);
            Debug.Assert(!wait2.IsCompleted, "Should be blocked by the existing wait");

            // Release the existing wait
            // After this point, wait2 may get completed or it may not (depending upon the execution of a ThreadPool item)
            semaphore.Release();         

            // If wait2 was not completed, it should now be cancelled
            cts1.Cancel();             

            if(wait2.Status == TaskStatus.RanToCompletion)
            {
                // Ignore this run; the lock was acquired before cancellation
                return;
            }

            var wasCanceled = false;
            try
            {
                await wait2.ConfigureAwait(false);

                // Ignore this run; this should only be hit if the wait lock was acquired
                return;
            }
            catch(OperationCanceledException)
            {
                wasCanceled = true;
            }

            Debug.Assert(wasCanceled, "Should have been canceled");            
            Debug.Assert(semaphore.CurrentCount > 0, "The first wait was released, and the second was canceled so why can no threads enter?");
        }
    }
}

А здесь ссылка на реализацию LINQPad.

Запустите предыдущий пример несколько раз, и иногда вы увидите отмену WaitAsync больше не позволяет никаким темам входить

Обновить

Похоже, что это не воспроизводится на каждой машине, если вам удается воспроизвести проблему, пожалуйста, оставьте комментарий о том, что так.

Мне удалось воспроизвести проблему на следующем:

  • 3x 64-битных компьютера с Windows 7 под управлением i7-2600
  • 64-битная машина Windows 8 под управлением i7-3630QM

Я не смог воспроизвести проблему на следующем:

  • 64-битная машина Windows 8 под управлением i5-2500k

Обновление 2

Я подал ошибку в Microsoft здесь, однако пока что они не могут воспроизвести, поэтому было бы очень полезно, если бы как можно больше людей попыталось запустить пример проекта, это можно найти на вкладке вложений связанной проблемы.

1 ответ

Решение

SemaphoreSlim был изменен в.NET 4.5.1

.NET 4.5 Версия метода WaitUntilCountOrTimeoutAsync:

private async Task<bool> WaitUntilCountOrTimeoutAsync(TaskNode asyncWaiter, int millisecondsTimeout, CancellationToken cancellationToken)
{ 
    [...]

    // If the await completed synchronously, we still hold the lock.  If it didn't, 
    // we no longer hold the lock.  As such, acquire it. 
    lock (m_lockObj)
    { 
        RemoveAsyncWaiter(asyncWaiter);
        if (asyncWaiter.IsCompleted)
        {
            Contract.Assert(asyncWaiter.Status == TaskStatus.RanToCompletion && asyncWaiter.Result, 
                "Expected waiter to complete successfully");
            return true; // successfully acquired 
        } 
        cancellationToken.ThrowIfCancellationRequested(); // cancellation occurred
        return false; // timeout occurred 
    }
}

Тот же метод в 4.5.1:

private async Task<bool> WaitUntilCountOrTimeoutAsync(TaskNode asyncWaiter, int millisecondsTimeout, CancellationToken cancellationToken)
{
    [...]

    lock (m_lockObj)
    {
        if (RemoveAsyncWaiter(asyncWaiter))
        {
            cancellationToken.ThrowIfCancellationRequested(); 
            return false; 
        }
    }

    return await asyncWaiter.ConfigureAwait(false);
}

asyncWaiter - это, по сути, задача, которая всегда возвращает true (завершается в отдельном потоке, всегда с результатом True).

Метод release вызывает RemoveAsyncWaiter и назначает для работника значение true.

Вот возможная проблема в 4.5:

    RemoveAsyncWaiter(asyncWaiter);
    if (asyncWaiter.IsCompleted)
    {
        Contract.Assert(asyncWaiter.Status == TaskStatus.RanToCompletion && asyncWaiter.Result, 
            "Expected waiter to complete successfully");
        return true; // successfully acquired 
    } 
    //! another thread calls Release
    //! asyncWaiter completes with true, Wait should return true
    //! CurrentCount will be 0

    cancellationToken.ThrowIfCancellationRequested(); // cancellation occurred, 
    //! throws OperationCanceledException
    //! wasCanceled will be true

    return false; // timeout occurred 

В 4.5.1 RemoveAsyncWaiter вернет false, а WaitAsync вернет true.

Другие вопросы по тегам