为什么我不能在locking语句的主体中使用“await”操作符?

C#(.NETasynchronousCTP)中的await关键字不允许来自locking语句中。

来自MSDN :

awaitexpression式不能用于同步函数,查询expression式,exception处理语句的catch或finally块, lock语句的块或不安全的上下文中。

我认为这对于编译器团队来说由于某种原因而难以实现。

我试图解决使用声明:

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的调用似乎无限期地(大部分时间)阻塞导致死锁,因为其他线程试图获取锁。 我怀疑我的工作的不可靠性和理由等待声明不允许在locking声明中有某种相关性。

有谁知道为什么在locking语句的正文中不允许等待?

我认为这对于编译器团队来说由于某种原因而难以实现。

不,实施起来并不困难或不可能 – 事实上,您自己实施这一事实certificate了这一事实。 相反, 这是一个非常糟糕的主意 ,所以我们不允许它,以保护你免于犯这个错误。

调用到ExitDisposable.Dispose内的Monitor.Exit似乎无限期地(大部分时间)阻塞导致死锁,因为其他线程试图获取锁。 我怀疑我的工作的不可靠性和理由等待声明不允许在locking声明中有某种相关性。

正确的,你已经发现我们为什么违法了。 在锁内等待是产生死锁的秘诀。

我相信你可以看到为什么: 在await返回控制给调用者的时间和方法恢复之间运行任意代码 。 该任意代码可能会取出产生locking顺序反转的锁,从而导致死锁。

更糟糕的是, 代码可能会在另一个线程上恢复 (在高级场景中;通常你会在执行await的线程上再次拿起,但不一定),在这种情况下,解锁将会解锁与线程不同的线程上的锁出门锁。 这是一个好主意吗? 没有。

我注意到,出于同样的原因,在lock内进行yield return也是“最差的做法”。 这样做是合法的,但是我希望我们已经把它变成非法的。 我们不会为“等待”而犯同样的错误。

使用SemaphoreSlim.WaitAsync方法。

  await mySemaphoreSlim.WaitAsync(); try { await Stuff(); } finally { mySemaphoreSlim.Release(); } 

基本上这是做错事。

有两种方式可以实现:

  • 保持locking,只在块的最后释放它
    这是一个非常糟糕的想法,因为你不知道asynchronous操作要花多长时间。 你应该只在最短的时间内持有锁。 这也是不可能的,因为线程拥有一个锁,而不是一个方法 – 甚至可能不会在同一个线程上执行其余的asynchronous方法(取决于任务调度器)。

  • 释放等待中的锁,并在等待返回时重新获取
    这违反了IMO最不惊讶的原则,即asynchronous方法应该像等效的同步代码那样尽可能地接近 – 除非在locking块中使用Monitor.Wait ,否则您希望在块的持续时间内拥有locking。

所以基本上这里有两个相互竞争的需求 – 你不应该试图在这里做第一个,如果你想采取第二种方法,可以通过由awaitexpression式分隔两个分离的锁块来使代码更清晰:

 // Now it's clear where the locks will be acquired and released lock (foo) { } var result = await something; lock (foo) { } 

所以,通过禁止你在锁块本身等待,这个语言迫使你去思考你真正想做什么,并且在你编写的代码中做出更清晰的select。

这反应到http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx,http://winrtstoragehelper.codeplex.com/,Windows 8app store和.net 4.5

这是我的angular度:

asynchronous/等待语言function使得许多事情变得相当容易,但是它也引入了一个在很容易使用asynchronous调用之前很less遇到的场景:reentrance。

对于事件处理程序来说尤其如此,因为对于许多事件,在从事件处理程序返回后,对于发生的事情没有任何线索。 可能发生的一件事情是,在第一个事件处理程序中等待的asynchronous方法从另一个事件处理程序仍然在同一个线程中调用。

下面是我在Windows 8 App Store应用程序中遇到的一个真实情况:我的应用程序有两个框架:进入和离开框架我想加载/安全一些数据到文件/存储。 OnNavigatedTo / From事件用于保存和加载。 保存和加载是由一些asynchronous实用程序function(如http://winrtstoragehelper.codeplex.com/ )完成的。 从第1帧到第2帧或另一个方向导航时,将调用并等待asynchronous加载和安全操作。 事件处理程序变成asynchronous返回void =>他们不能等待。

但是,该实用程序的第一个文件打开操作(让:在一个保存function内)也是asynchronous的,所以第一个await将控制权交还给框架,后来有时候通过第二个事件处理程序调用其他实用程序(加载)。 现在加载试图打开相同的文件,如果现在保存操作打开该文件,则会失败并显示ACCESSDENIEDexception。

对我来说最小的解决scheme是通过使用和AsyncLock保护文件访问。

 private static readonly AsyncLock m_lock = new AsyncLock(); ... using (await m_lock.LockAsync()) { file = await folder.GetFileAsync(fileName); IRandomAccessStream readStream = await file.OpenAsync(FileAccessMode.Read); using (Stream inStream = Task.Run(() => readStream.AsStreamForRead()).Result) { return (T)serializer.Deserialize(inStream); } } 

请注意,他的锁基本上locking所有的文件操作的公用事业只有一个锁,这是不必要的强大,但对我的情况工作正常。

这里是我的testing项目:一个Windows 8应用程序商店应用程序与一些testing呼吁的原始版本从http://winrtstoragehelper.codeplex.com/和我的修改版本使用从Stephen Toub的AsyncLock http://blogs.msdn。 com / b / pfxteam / archive / 2012/02/12 / 10266988.aspx 。

我也可以build议这个链接: http : //www.hanselman.com/blog/ComparingTwoTechniquesInNETAsynchronousCoordinationPrimitives.aspx

嗯,看起来丑陋,似乎工作。

 static class Async { public static Task<IDisposable> Lock(object obj) { return TaskEx.Run(() => { var resetEvent = ResetEventFor(obj); resetEvent.WaitOne(); resetEvent.Reset(); return new ExitDisposable(obj) as IDisposable; }); } private static readonly IDictionary<object, WeakReference> ResetEventMap = new Dictionary<object, WeakReference>(); private static ManualResetEvent ResetEventFor(object @lock) { if (!ResetEventMap.ContainsKey(@lock) || !ResetEventMap[@lock].IsAlive) { ResetEventMap[@lock] = new WeakReference(new ManualResetEvent(true)); } return ResetEventMap[@lock].Target as ManualResetEvent; } private static void CleanUp() { ResetEventMap.Where(kv => !kv.Value.IsAlive) .ToList() .ForEach(kv => ResetEventMap.Remove(kv)); } private class ExitDisposable : IDisposable { private readonly object _lock; public ExitDisposable(object @lock) { _lock = @lock; } public void Dispose() { ResetEventFor(_lock).Set(); } ~ExitDisposable() { CleanUp(); } } } 

Stephen Taub已经为此问题实现了一个解决scheme,请参阅构buildasynchronous协调基元,第7部分:AsyncReaderWriterLock 。

斯蒂芬·陶布在业内备受推崇,所以他写的任何东西都可能是坚实的。

我不会重现他在他的博客上发布的代码,但是我将展示如何使用它:

 /// <summary> /// Demo class for reader/writer lock that supports async/await. /// For source, see Stephen Taub's brilliant article, "Building Async Coordination /// Primitives, Part 7: AsyncReaderWriterLock". /// </summary> public class AsyncReaderWriterLockDemo { private readonly IAsyncReaderWriterLock _lock = new AsyncReaderWriterLock(); public async void DemoCode() { using(var releaser = await _lock.ReaderLockAsync()) { // Insert reads here. // Multiple readers can access the lock simultaneously. } using (var releaser = await _lock.WriterLockAsync()) { // Insert writes here. // If a writer is in progress, then readers are blocked. } } } 

如果你在.NET框架中使用SemaphoreSlim.WaitAsync 。 你不会得到一个读者/写者的锁,但你会得到尝试和testing的实现。

我曾尝试使用监视器(下面的代码)似乎工作,但有一个GOTCHA …当你有多个线程,它会给… System.Threading.SynchronizationLockException对象同步方法是从一个非同步的代码块调用。

 using System; using System.Threading; using System.Threading.Tasks; namespace MyNamespace { public class ThreadsafeFooModifier : { private readonly object _lockObject; public async Task<FooResponse> ModifyFooAsync() { FooResponse result; Monitor.Enter(_lockObject); try { result = await SomeFunctionToModifyFooAsync(); } finally { Monitor.Exit(_lockObject); } return result; } } } 

在此之前,我只是这样做,但它是在ASP.NET控制器,所以导致了一个死锁。

public async Task<FooResponse> ModifyFooAsync() { lock(lockObject) { return SomeFunctionToModifyFooAsync.Result; } }