不允许在lock
语句中使用C#(. NET Async CTP)中的await
关键字。
从MSDN开始:
- await表达式不能用于**同步函数、查询表达式、异常处理语句的catch或finally块、lock语句的块或不安全上下文。
我假设由于某种原因,编译器团队很难或不可能实现这一点。
我尝试了using语句的变通方法:
class Async
{
public static async Task<IDisposable> Lock(object obj)
{
while (!Monitor.TryEnter(obj))
await TaskEx.Yield();
return new ExitDisposable(obj);
}
private class ExitDisposable : IDisposable
{
private readonly object obj;
public ExitDisposable(object obj) { this.obj = obj; }
public void Dispose() { Monitor.Exit(this.obj); }
}
}
// example usage
using (await Async.Lock(padlock))
{
await SomethingAsync();
}
然而,这并不像预期的那样工作。在ExitDisposable.Dispose
中对Monitor.Exit
的调用似乎无限期地阻塞(大多数时间),导致死锁,因为其他线程试图获取锁。我怀疑我的工作不可靠,以及lock
语句中不允许await
语句的原因在某种程度上是相关的。
有人知道 * 为什么 * await
不允许出现在lock
语句的主体中吗?
9条答案
按热度按时间rxztt3cl1#
我假设由于某种原因,编译器团队很难或不可能实现这一点。
不,它一点也不难或不可能实现--你自己实现它的事实就证明了这一点。相反,这是一个难以置信的坏主意,所以我们不允许它,以保护你避免犯这个错误。
在ExitDisposable.Dispose中调用Monitor.Exit似乎会无限期地阻塞(大多数时候),从而在其他线程尝试获取锁时导致死锁。我怀疑我的工作不可靠以及在lock语句中不允许使用wait语句的原因在某种程度上是相关的。
正确,你已经发现了为什么我们把它定为非法。* 在锁里等待是产生死锁的秘诀。*
我相信你能明白为什么:* 任意代码在await将控制权返回给调用者和方法恢复之间运行 。该任意代码可能正在取出会产生锁顺序反转的锁,从而导致死锁。
更糟糕的是, 代码可能会在另一个线程上继续 *(在高级场景中;通常你会在执行等待的线程上再次拾取,但不一定)在这种情况下,解锁将是在另一个线程上解锁,而不是在取出锁的线程上解锁,这是个好主意吗?
我注意到,出于同样的原因,在
lock
中执行yield return
也是“最坏的做法”。这样做是法律的的,但我希望我们已经将其定为非法。我们不会犯同样的“等待”错误。7lrncoxx2#
使用
SemaphoreSlim.WaitAsync
方法。6jygbczu3#
基本上这是错误的做法。
有两种方式 * 可以 * 实现这一点:
*保持锁定,仅在块结束时释放它。
这是一个非常糟糕的主意,因为你不知道异步操作要花多长时间。你应该只在 * 最短 * 的时间内持有锁。这也可能是不可能的,因为一个 * 线程 * 拥有一个锁,而不是一个方法--你甚至不可能在同一个线程上执行异步方法的其余部分(取决于任务调度器)。
*在await中释放锁,并在await返回时重新获取锁
这违反了IMO的最小惊奇原则,即异步方法的行为应该尽可能接近于等效的同步代码--除非您在锁块中使用
Monitor.Wait
,否则您希望在块的持续时间内拥有锁。所以这里基本上有两个相互竞争的要求--你不应该 * 尝试 * 在这里做第一个,如果你想采取第二种方法,你可以通过用await表达式分隔两个单独的锁块来使代码更清晰:
因此,通过禁止您在锁块本身中等待,该语言迫使您考虑您“真正”想要做什么,并在您编写的代码中使该选择更加清晰。
uujelgoq4#
这只是用户1639030回答的一个扩展。
基本版本
扩展版本
LockAsync
方法的一个版本,声称是完全死锁安全的(来自Jez建议的4th revision)。jk9hmnmh5#
这指的是Building Async Coordination Primitives, Part 6: AsyncLock、http://winrtstoragehelper.codeplex.com/、Windows 8应用程序商店和.net 4.5
以下是我的观点:
async/await语言特性使许多事情变得相当简单,但它也引入了一个在使用异步调用如此简单之前很少遇到的场景:重新进入。
对于事件处理器来说尤其如此,因为对于许多事件,你从事件处理器返回后,你对发生的事情没有任何线索。实际上可能发生的一件事是,你在第一个事件处理器中等待的异步方法,被仍然在同一线程上的另一个事件处理器调用。
下面是我在Windows 8应用商店应用中遇到的一个真实的场景:我的应用有两个框架:进入和离开一个框架,我想加载/安全一些数据到文件/存储。OnNavigatedTo/From事件用于保存和加载。保存和加载是由一些异步实用函数完成的(如http://winrtstoragehelper.codeplex.com/)。当从帧1导航到帧2或在其它方向上导航时,异步加载和安全操作被调用并等待。2事件处理程序变成异步返回void =〉,它们不能被等待。
但是,第一个文件打开操作(比方说:在一个保存函数中)也是异步的,所以第一个await将控制权返回给框架,框架稍后通过第二个事件处理程序调用另一个实用程序(load)。load现在尝试打开同一个文件,如果文件现在已经打开用于保存操作,则会失败,并出现ACCESSDENIED异常。
对我来说,最低限度的解决方案是通过using和AsyncLock来保护文件访问。
请注意,他的锁基本上只使用一个锁就锁定了该实用程序的所有文件操作,这是不必要的强锁,但对我的场景来说很好用。
Here是我的测试项目:一个Windows 8应用商店应用程序,对X1 E4 F1 X的原始版本和我使用X1 E5 F1 X的异步锁的修改版本进行了一些测试调用。
我也可以建议这个链接:http://www.hanselman.com/blog/ComparingTwoTechniquesInNETAsynchronousCoordinationPrimitives.aspx
vfwfrxfs6#
Stephen Taub已经实现了此问题的解决方案,请参见Building Async Coordination Primitives, Part 7: AsyncReaderWriterLock。
斯蒂芬·陶布在业内备受推崇,所以他写的任何东西都很可能是扎实的。
我不会复制他在博客上发布的代码,但我将向您展示如何使用它:
如果你想把一个方法集成到.NET框架中,那么就使用
SemaphoreSlim.WaitAsync
,你不会得到一个读写器锁,但是你会得到一个久经考验的实现。ibps3vxo7#
嗯,看起来很丑,但似乎有用。
kiayqfof8#
受Stephen Toub的AsyncLock实现(在this blog post上讨论)的启发,我创建了一个
MutexAsyncable
类,它可以直接替代同步或异步代码中的lock
语句:如果您使用的是.NET 5+,那么使用上面的代码是安全的,因为它不会抛出
ThreadAbortException
。受this answer的启发,我还创建了一个扩展的
SemaphoreLocker
类,它可以作为lock
的通用替代品,可以同步或异步使用,它比上面的MutexAsyncable
效率低,但分配了更多的资源,尽管它具有强制辅助代码在完成后释放锁的优点(从技术上讲,MutexAsyncable
返回的IDisposable
无法通过调用代码进行处理,从而导致死锁)。它还有额外的try/finally代码来处理ThreadAbortException
的可能性,因此应该可以在早期的.NET版本中使用:mlnl4t2r9#
我试过使用一个
Monitor
(下面的代码),它看起来可以工作,但有一个GOTCHA...当你有多个线程时,它会给予...从未同步的代码块调用了
System.Threading.SynchronizationLockException
对象同步方法。在此之前,我只是简单地这样做,但它是在一个ASP.NET控制器,所以它导致了死锁。