Отмена семафораSlim.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

СемафорSlim был изменен в .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).

Метод деблокирования вызывает метод 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.