.net 为什么不能在lock语句体中使用'await'操作符?

bttbmeg0  于 2023-03-04  发布在  .NET
关注(0)|答案(9)|浏览(698)

不允许在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语句的主体中吗?

rxztt3cl

rxztt3cl1#

我假设由于某种原因,编译器团队很难或不可能实现这一点。
不,它一点也不难或不可能实现--你自己实现它的事实就证明了这一点。相反,这是一个难以置信的坏主意,所以我们不允许它,以保护你避免犯这个错误。
在ExitDisposable.Dispose中调用Monitor.Exit似乎会无限期地阻塞(大多数时候),从而在其他线程尝试获取锁时导致死锁。我怀疑我的工作不可靠以及在lock语句中不允许使用wait语句的原因在某种程度上是相关的。
正确,你已经发现了为什么我们把它定为非法。* 在锁里等待是产生死锁的秘诀。*
我相信你能明白为什么:* 任意代码在await将控制权返回给调用者和方法恢复之间运行 。该任意代码可能正在取出会产生锁顺序反转的锁,从而导致死锁。
更糟糕的是,
代码可能会在另一个线程上继续 *(在高级场景中;通常你会在执行等待的线程上再次拾取,但不一定)在这种情况下,解锁将是在另一个线程上解锁,而不是在取出锁的线程上解锁,这是个好主意吗?
我注意到,出于同样的原因,在lock中执行yield return也是“最坏的做法”。这样做是法律的的,但我希望我们已经将其定为非法。我们不会犯同样的“等待”错误。

7lrncoxx

7lrncoxx2#

使用SemaphoreSlim.WaitAsync方法。

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

6jygbczu3#

基本上这是错误的做法。
有两种方式 * 可以 * 实现这一点:

*保持锁定,仅在块结束时释放它

这是一个非常糟糕的主意,因为你不知道异步操作要花多长时间。你应该只在 * 最短 * 的时间内持有锁。这也可能是不可能的,因为一个 * 线程 * 拥有一个锁,而不是一个方法--你甚至不可能在同一个线程上执行异步方法的其余部分(取决于任务调度器)。

*在await中释放锁,并在await返回时重新获取锁

这违反了IMO的最小惊奇原则,即异步方法的行为应该尽可能接近于等效的同步代码--除非您在锁块中使用Monitor.Wait,否则您希望在块的持续时间内拥有锁。
所以这里基本上有两个相互竞争的要求--你不应该 * 尝试 * 在这里做第一个,如果你想采取第二种方法,你可以通过用await表达式分隔两个单独的锁块来使代码更清晰:

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

因此,通过禁止您在锁块本身中等待,该语言迫使您考虑您“真正”想要做什么,并在您编写的代码中使该选择更加清晰。

uujelgoq

uujelgoq4#

这只是用户1639030回答的一个扩展。

基本版本

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

扩展版本

LockAsync方法的一个版本,声称是完全死锁安全的(来自Jez建议的4th revision)。

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. 
        });
    }
}
jk9hmnmh

jk9hmnmh5#

这指的是Building Async Coordination Primitives, Part 6: AsyncLockhttp://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来保护文件访问。

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

请注意,他的锁基本上只使用一个锁就锁定了该实用程序的所有文件操作,这是不必要的强锁,但对我的场景来说很好用。
Here是我的测试项目:一个Windows 8应用商店应用程序,对X1 E4 F1 X的原始版本和我使用X1 E5 F1 X的异步锁的修改版本进行了一些测试调用。
我也可以建议这个链接:http://www.hanselman.com/blog/ComparingTwoTechniquesInNETAsynchronousCoordinationPrimitives.aspx

vfwfrxfs

vfwfrxfs6#

Stephen Taub已经实现了此问题的解决方案,请参见Building Async Coordination Primitives, Part 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,你不会得到一个读写器锁,但是你会得到一个久经考验的实现。

ibps3vxo

ibps3vxo7#

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

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

kiayqfof8#

受Stephen Toub的AsyncLock实现(在this blog post上讨论)的启发,我创建了一个MutexAsyncable类,它可以直接替代同步或异步代码中的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
this answer的启发,我还创建了一个扩展的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();
    }
}
mlnl4t2r

mlnl4t2r9#

我试过使用一个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;
    }
}

相关问题