为什么我不能在 lock 语句的正文中使用"await"运算符?



c#中的await关键字。. NET异步CTP)在lock语句中是不允许的。

从MSDN:

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,否则您希望在块的持续时间内拥有锁。

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

// 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;
    }
}

最新更新