为什么我不能用'wait'操作符在锁语句体中?

c#中的await关键字。NET Async CTP)不允许在锁语句中使用。

MSDN:

< p > < >强 Await表达式不能用于同步函数和查询中的 表达式,在异常处理的catch或finally块中

.语句,在锁语句的块中,或在不安全的上下文中

我认为由于某些原因,编译器团队很难或不可能实现这一点。

我尝试使用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();
}

然而,这并没有像预期的那样工作。对Monitor的调用。在ExitDisposable中退出。Dispose似乎无限期地阻塞(大多数时间),当其他线程试图获得锁时,会导致死锁。我怀疑我周围工作的不可靠性和原因等待语句不允许在锁语句以某种方式相关。

有人知道为什么 await在锁语句体中是不允许的吗?

161885 次浏览

基本上这是错误的做法。

可以有两种实现方式:

    <李> < p > 保持锁,只在区块结束时释放它。< br > 这是一个非常糟糕的主意,因为您不知道异步操作将花费多长时间。你应该只持有锁最小的的时间。这也可能是不可能的,因为线程拥有一个锁,而不是一个方法——你甚至可能不会在同一个线程上执行异步方法的其余部分(取决于任务调度器) <李> < p > 释放await中的锁,并在await返回时重新获取它 < br > 这违反了IMO的最小惊讶原则,其中异步方法的行为应该尽可能类似于等效的同步代码-除非你在锁块中使用Monitor.Wait,否则你希望在块的持续时间内拥有锁

所以基本上这里有两个相互竞争的要求——你不应该用尝试来执行第一个要求,如果你想采用第二个方法,你可以通过使用await表达式分隔两个分离的锁块来使代码更加清晰:

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

因此,通过禁止你在锁块中等待,该语言迫使你思考你真的想要做什么,并在你编写的代码中使这个选择更清楚。

我认为由于某些原因,编译器团队很难或不可能实现这一点。

不,实现它一点也不困难或不可能——你自己实现它的事实证明了这一点。更确切地说,这是一个非常糟糕的主意,所以我们不允许它,以保护你犯这个错误。

呼叫监视器。在ExitDisposable中退出。Dispose似乎无限期地阻塞(大多数时间),当其他线程试图获得锁时,会导致死锁。我怀疑我周围工作的不可靠性和原因等待语句不允许在锁语句以某种方式相关。

没错,你已经知道我们为什么把它定为非法了。在锁内等待是产生死锁的一种方法。

我相信你能明白为什么:在await将控制返回给调用者和方法恢复之间运行任意代码。这种任意代码可能会删除产生锁序反转的锁,从而导致死锁。

更糟糕的是,代码可以在另一个线程上继续(在高级场景中;通常情况下,您将再次在执行await的线程上进行操作,但不一定),在这种情况下,unlock将在与取出锁的线程不同的线程上解锁锁。这是个好主意吗?不。

我注意到,出于同样的原因,在lock中执行yield return也是一种“最糟糕的做法”。这样做是合法的,但我希望我们把它定为非法。我们不会在await上犯同样的错误。

这指的是构建异步协调原语,第6部分:AsyncLockhttp://winrtstoragehelper.codeplex.com/, Windows 8应用程序商店和。net 4.5

以下是我的观点:

async/await语言特性使许多事情变得相当简单,但它也引入了一个场景 在使用异步调用之前很少遇到:reentry .

对于事件处理程序来说尤其如此,因为对于许多事件,在从事件处理程序返回后,您根本不知道发生了什么。 可能发生的一件事是,你在第一个事件处理程序中等待的异步方法,从另一个事件处理程序中调用 同一线程。< / p > 下面是我在windows 8应用商店应用中遇到的一个真实场景: 我的应用程序有两个框架:进入和离开一个框架,我想加载/安全一些数据到文件/存储。 OnNavigatedTo/From事件用于保存和加载。保存和加载是由一些异步实用程序函数(如http://winrtstoragehelper.codeplex.com/)完成的。 当从帧1导航到帧2或其他方向时,异步加载和安全操作被调用并等待。 事件处理程序变成异步返回void =>他们不能被等待。

然而,该实用程序的第一个文件打开操作(让我们说:在保存函数内)也是异步的 因此,第一个await将控制返回给框架,框架稍后通过第二个事件处理程序调用另一个实用程序(load)。 加载现在尝试打开相同的文件,如果 文件现在已经打开用于保存操作,失败并出现ACCESSDENIED异常

对我来说,一个最小的解决方案是通过using和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);
}
}

请注意,他的锁基本上只用一个锁就锁定了该实用程序的所有文件操作,这是不必要的强大,但在我的场景中工作得很好。

在这里是我的测试项目:一个windows 8应用程序商店应用程序,对来自http://winrtstoragehelper.codeplex.com/的原始版本和使用Stephen Toub的AsyncLock的修改版本进行了一些测试调用。

我还可以建议这个链接: http://www.hanselman.com/blog/ComparingTwoTechniquesInNETAsynchronousCoordinationPrimitives.aspx < / p >

嗯,看起来很丑,但似乎有用。

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();
}
}
}

使用SemaphoreSlim.WaitAsync方法。

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

Stephen Taub已经实现了这个问题的解决方案,参见构建异步协调原语,第7部分:AsyncReaderWriterLock

Stephen Taub在业内备受推崇,所以他写的任何东西都可能是可靠的。

我不会复制他在博客上发布的代码,但我会告诉你如何使用它:

/// <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。您不会得到读取器/写入器锁,但是您将得到经过尝试和测试的实现。

我确实尝试使用一个监视器(下面的代码),它似乎可以工作,但有一个GOTCHA…当你有多个线程时,它会给出。对象同步方法从未同步的代码块调用。

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<ModifyFooAsync () { 锁(lockObject) { 返回SomeFunctionToModifyFooAsync.Result; } 代码}< / > < / p >

这只是一个扩展这个答案

using System;
using System.Threading;
using System.Threading.Tasks;


public class SemaphoreLocker
{
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);


public async Task LockAsync(Func<Task> worker)
{
await _semaphore.WaitAsync();
try
{
await worker();
}
finally
{
_semaphore.Release();
}
}


// overloading variant for non-void methods with return type (generic T)
public async Task<T> LockAsync<T>(Func<Task<T>> worker)
{
await _semaphore.WaitAsync();
try
{
return await worker();
}
finally
{
_semaphore.Release();
}
}
}

用法:

public class Test
{
private static readonly SemaphoreLocker _locker = new SemaphoreLocker();


public async Task DoTest()
{
await _locker.LockAsync(async () =>
{
// [async] calls can be used within this block
// to handle a resource by one thread.
});
// OR
var result = await _locker.LockAsync(async () =>
{
// [async] calls can be used within this block
// to handle a resource by one thread.
});
}
}