为什么我不能使用'await'锁语句体中的操作符
本文关键字:语句 操作符 await 不能 为什么 | 更新日期: 2023-09-27 18:10:12
c#中的await
关键字。. NET异步CTP)在lock
语句中是不允许的。
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();
}
然而,这并没有像预期的那样工作。在ExitDisposable.Dispose
中对Monitor.Exit
的调用似乎无限期地阻塞(大多数时候),因为其他线程试图获取锁而导致死锁。我怀疑我的工作的不可靠性和lock
语句不允许await
语句的原因在某种程度上是相关的。
有人知道为什么 await
不允许在lock
语句的主体内吗?
我认为这对于编译器团队来说,由于某种原因是很难或不可能实现的。
不,它一点也不困难或不可能实现——你自己实现它的事实就是对这一事实的证明。更确切地说,这是一个非常糟糕的主意,所以我们不允许这样做,以保护您避免犯这个错误。
调用Monitor。在ExitDisposable中退出。Dispose似乎会无限期地阻塞(大多数时候),在其他线程试图获取该锁时导致死锁。我怀疑我工作的不可靠性和锁语句中不允许使用await语句的原因在某种程度上是相关的。
正确,你已经知道为什么我们把它定为非法了。在锁内等待是产生死锁的一个方法。
我相信你可以看到原因:任意代码在await返回控制给调用者和方法恢复之间运行。任意代码可能会取出产生锁顺序反转的锁,从而产生死锁。
更糟的是,代码可能会在另一个线程上恢复(在高级场景中;通常情况下,您会再次选择执行等待的线程(但不一定),在这种情况下,解锁将在与取出锁的线程不同的线程上解锁锁。这是个好主意吗?没有。
我注意到,出于同样的原因,在lock
中执行yield return
也是一种"最糟糕的做法"。这样做是合法的,但我希望我们把它定为非法。我们不会在await中犯同样的错误。
使用SemaphoreSlim.WaitAsync
方法。
await mySemaphoreSlim.WaitAsync();
try {
await Stuff();
} finally {
mySemaphoreSlim.Release();
}
这只是user1639030回答的扩展。
基本版
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.
});
}
}
<
扩展版本/strong>
声称完全死锁安全的LockAsync
方法的版本(来自Jez建议的第4版)。
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)
{
var isTaken = false;
try
{
do
{
try
{
}
finally
{
isTaken = await _semaphore.WaitAsync(TimeSpan.FromSeconds(1));
}
}
while (!isTaken);
await worker();
}
finally
{
if (isTaken)
{
_semaphore.Release();
}
}
}
// overloading variant for non-void methods with return type (generic T)
public async Task<T> LockAsync<T>(Func<Task<T>> worker)
{
var isTaken = false;
try
{
do
{
try
{
}
finally
{
isTaken = await _semaphore.WaitAsync(TimeSpan.FromSeconds(1));
}
}
while (!isTaken);
return await worker();
}
finally
{
if (isTaken)
{
_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.
});
}
}
这样做基本上是错误的。
有两种实现方式:保持锁,只在块结束时释放它
这是一个非常糟糕的主意,因为您不知道异步操作将花费多长时间。您应该只在最小的时间内持有锁。这也可能是不可能的,因为线程拥有一个锁,而不是一个方法——你甚至可能不会在同一个线程上执行异步方法的其余部分(取决于任务调度程序)。在等待中释放锁,并在等待返回时重新获得锁
这违反了IMO的最小惊奇原则,异步方法的行为应该尽可能接近于等效的同步代码——除非您在锁块中使用Monitor.Wait
,否则您希望在块的持续时间内拥有锁。
// Now it's clear where the locks will be acquired and released
lock (foo)
{
}
var result = await something;
lock (foo)
{
}
因此,通过禁止在锁块本身中等待,语言迫使您思考真正想要做什么,并在您编写的代码中使该选择更清晰。
这是指构建异步协调原语,第6部分:AsyncLock, http://winrtstoragehelper.codeplex.com/, Windows 8应用商店和。net 4.5
我的观点是:
async/await语言特性使许多事情变得相当容易,但它也引入了一个场景以前很少遇到这么容易使用异步调用:reentry .
对于事件处理程序尤其如此,因为对于许多事件,您不知道从事件处理程序返回后发生了什么。可能会发生的一件事是,你在第一个事件处理程序中等待的async方法,会从另一个事件处理程序中被调用同一线程。
以下是我在windows 8 App store应用中遇到的一个真实场景:我的应用程序有两个帧:进入和离开一个帧,我想加载/安全的一些数据到文件/存储。OnNavigatedTo/From事件用于保存和加载。保存和加载由一些异步实用程序函数完成(如http://winrtstoragehelper.codeplex.com/)。当从第1帧导航到第2帧或其他方向时,异步加载和安全操作被调用并等待。事件处理程序变为异步返回void =>然而,该实用程序的第一个文件打开操作(let说:在保存函数中)也是异步的因此,第一个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
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
代替。你不会得到一个读/写锁,但是你会得到一个经过测试的实现。
嗯,看起来很丑,但似乎行得通。
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();
}
}
}
我创建了一个MutexAsyncable
类,灵感来自Stephen Toub的AsyncLock实现(在这篇博客文章中讨论),它可以用作同步或异步代码中lock
语句的替代:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace UtilsCommon.Lib;
/// <summary>
/// Class that provides (optionally async-safe) locking using an internal semaphore.
/// Use this in place of a lock() {...} construction.
/// Bear in mind that all code executed inside the worker must finish before the next
/// thread is able to start executing it, so long-running code should be avoided inside
/// the worker if at all possible.
///
/// Example usage for sync:
/// using (mutex.LockSync()) {
/// // ... code here which is synchronous and handles a shared resource ...
/// return[ result];
/// }
///
/// ... or for async:
/// using (await mutex.LockAsync()) {
/// // ... code here which can use await calls and handle a shared resource ...
/// return[ result];
/// }
/// </summary>
public sealed class MutexAsyncable {
#region Internal classes
private sealed class Releaser : IDisposable {
private readonly MutexAsyncable _toRelease;
internal Releaser(MutexAsyncable toRelease) { _toRelease = toRelease; }
public void Dispose() { _toRelease._semaphore.Release(); }
}
#endregion
private readonly SemaphoreSlim _semaphore = new(1, 1);
private readonly Task<IDisposable> _releaser;
public MutexAsyncable() {
_releaser = Task.FromResult((IDisposable)new Releaser(this));
}
public IDisposable LockSync() {
_semaphore.Wait();
return _releaser.Result;
}
public Task<IDisposable> LockAsync() {
var wait = _semaphore.WaitAsync();
if (wait.IsCompleted) { return _releaser; }
else {
// Return Task<IDisposable> which completes once WaitAsync does
return wait.ContinueWith(
(_, state) => (IDisposable)state!,
_releaser.Result,
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default
);
}
}
}
如果你使用的是。net 5+,那么使用上面的代码是安全的,因为它不会抛出ThreadAbortException
。
SemaphoreLocker
类,它可以作为lock
的通用替代品,既可以同步使用,也可以异步使用。它的效率低于上面的MutexAsyncable
,并且分配了更多的资源,尽管它的好处是在完成锁后强制工作代码释放锁(从技术上讲,MutexAsyncable
返回的IDisposable
不能被调用代码处理,从而导致死锁)。它还有额外的try/finally代码来处理ThreadAbortException
的可能性,因此应该可以在早期的。net版本中使用:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace UtilsCommon.Lib;
/// <summary>
/// Class that provides (optionally async-safe) locking using an internal semaphore.
/// Use this in place of a lock() {...} construction.
/// Bear in mind that all code executed inside the worker must finish before the next thread is able to
/// start executing it, so long-running code should be avoided inside the worker if at all possible.
///
/// Example usage:
/// [var result = ]await _locker.LockAsync(async () => {
/// // ... code here which can use await calls and handle a shared resource one-thread-at-a-time ...
/// return[ result];
/// });
///
/// ... or for sync:
/// [var result = ]_locker.LockSync(() => {
/// // ... code here which is synchronous and handles a shared resource one-thread-at-a-time ...
/// return[ result];
/// });
/// </summary>
public sealed class SemaphoreLocker : IDisposable {
private readonly SemaphoreSlim _semaphore = new(1, 1);
/// <summary>
/// Runs the worker lambda in a locked context.
/// </summary>
/// <typeparam name="T">The type of the worker lambda's return value.</typeparam>
/// <param name="worker">The worker lambda to be executed.</param>
public T LockSync<T>(Func<T> worker) {
var isTaken = false;
try {
do {
try {
}
finally {
isTaken = _semaphore.Wait(TimeSpan.FromSeconds(1));
}
}
while (!isTaken);
return worker();
}
finally {
if (isTaken) {
_semaphore.Release();
}
}
}
/// <inheritdoc cref="LockSync{T}(Func{T})" />
public void LockSync(Action worker) {
var isTaken = false;
try {
do {
try {
}
finally {
isTaken = _semaphore.Wait(TimeSpan.FromSeconds(1));
}
}
while (!isTaken);
worker();
}
finally {
if (isTaken) {
_semaphore.Release();
}
}
}
/// <summary>
/// Runs the worker lambda in an async-safe locked context.
/// </summary>
/// <typeparam name="T">The type of the worker lambda's return value.</typeparam>
/// <param name="worker">The worker lambda to be executed.</param>
public async Task<T> LockAsync<T>(Func<Task<T>> worker) {
var isTaken = false;
try {
do {
try {
}
finally {
isTaken = await _semaphore.WaitAsync(TimeSpan.FromSeconds(1));
}
}
while (!isTaken);
return await worker();
}
finally {
if (isTaken) {
_semaphore.Release();
}
}
}
/// <inheritdoc cref="LockAsync{T}(Func{Task{T}})" />
public async Task LockAsync(Func<Task> worker) {
var isTaken = false;
try {
do {
try {
}
finally {
isTaken = await _semaphore.WaitAsync(TimeSpan.FromSeconds(1));
}
}
while (!isTaken);
await worker();
}
finally {
if (isTaken) {
_semaphore.Release();
}
}
}
/// <summary>
/// Releases all resources used by the current instance of the SemaphoreLocker class.
/// </summary>
public void Dispose() {
_semaphore.Dispose();
}
}
我确实尝试使用Monitor
(下面的代码),它似乎可以工作,但有一个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;
}
}