在我们的一个课程中,我们大量使用
SemaphoreSlim.WaitAsync(CancellationToken)
并取消它.
在调用SemaphoreSlim.Release()
(不久之后,我的意思是在ThreadPool有机会处理排队的项目之前)等待等待WaitAsync的调用时,我似乎遇到了一个问题,它使信号量处于不存在的状态可以获取进一步的锁.
由于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?"); } } }
运行上一个示例几次,有时您会看到WaitAsync的取消不再允许任何线程进入.
更新
看来这是不可重现的每台机器,如果你设法重现的问题,请留下这样的评论.
我已经设法重现了以下问题:
>运行i7-2600的3x 64位Windows 7机器
> 64位Windows 8机器运行i7-3630QM
我以前无法重现这个问题:
> 64位Windows 8机器运行i5-2500k
更新2
我已经提交了Microsoft here的错误,但到目前为止,他们无法重现,所以如果尽可能多的尝试和运行示例项目,它可能会有帮助,它可以在链接问题的附件选项卡上找到.
解决方法
.NET 4.5.1中更改了SemaphoreSlim
.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,CancellationToken cancellationToken) { [...] lock (m_lockObj) { if (RemoveAsyncWaiter(asyncWaiter)) { cancellationToken.ThrowIfCancellationRequested(); return false; } } return await asyncWaiter.ConfigureAwait(false); }
asyncWaiter基本上是一个总是返回true的任务(在单独的线程中完成,总是使用True结果).
释放方法调用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.