取消SemaphoreSlim.WaitAsync保持信号量锁定

时间:2014-01-09 12:11:22

标签: c# .net-4.5 async-await semaphore base-class-library

在我们的一个课程中,我们大量使用SemaphoreSlim.WaitAsync(CancellationToken)并取消它。

在致电SemaphoreSlim.Release()后不久取消对WaitAsync的待处理通话时,我似乎遇到了问题(很快,我的意思是在ThreadPool有机会之前处理排队的项目),它将信号量置于不能获得进一步锁定的状态。

由于在ThreadPoolRelease()的调用之间是否执行了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?");
        }
    }
}

here是LINQPad实现的链接。

运行上一个示例几次,有时您会看到取消WaitAsync不再允许任何线程进入。

更新

看来这在每台机器上都不可重复,如果您设法重现问题,请发表评论。

我已设法重现以下问题:

  • 运行i7-2600的3x 64位Windows 7计算机
  • 运行i7-3630QM的64位Windows 8计算机

我无法重现以下问题:

  • 运行i5-2500k的64位Windows 8计算机

更新2

我已经向Microsoft here提交了一个错误,但到目前为止他们无法重现,所以如果尽可能多的尝试运行示例项目它会真的有用,它可以在附件中找到链接问题的标签。

1 个答案:

答案 0 :(得分: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并调度worker以完成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。