为什么我不能使用“await”锁定语句体内的运算符?

发布于 2024-12-07 20:31:03 字数 1293 浏览 1 评论 0原文

lock 语句中不允许使用 C# (.NET Async CTP) 中的 await 关键字。

来自 MSDN

安 wait 表达式不能在同步函数、查询中使用 表达式,在异常处理的 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 语句体内使用?

The await keyword in C# (.NET Async CTP) is not allowed from within a lock statement.

From MSDN:

An
await expression cannot be used
in a synchronous function, in a query
expression, in the catch or finally block of an exception handling
statement, in the block of a lock statement, or in an unsafe context.

I assume this is either difficult or impossible for the compiler team to implement for some reason.

I attempted a work around with the using statement:

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

However this does not work as expected. The call to Monitor.Exit within ExitDisposable.Dispose seems to block indefinitely (most of the time) causing deadlocks as other threads attempt to acquire the lock. I suspect the unreliability of my work around and the reason await statements are not allowed in lock statement are somehow related.

Does anyone know why await isn't allowed within the body of a lock statement?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(10

挽梦忆笙歌 2024-12-14 20:31:03

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

不,实施起来一点也不困难或不可能——您自己实施的事实就证明了这一事实。相反,这是一个非常糟糕的主意,因此我们不允许这样做,以防止您犯此错误。

在 ExitDisposable.Dispose 中调用 Monitor.Exit 似乎会无限期地阻塞(大多数情况下),当其他线程尝试获取锁时会导致死锁。我怀疑我的工作的不可靠性和lock语句中不允许await语句的原因在某种程度上是相关的。

是的,您已经发现为什么我们将其定为非法。 在锁内等待是产生死锁的一个因素。

我相信您可以明白原因:在等待将控制权返回给调用者和方法恢复之间运行任意代码 >。该任意代码可能会取出产生锁顺序反转的锁,从而导致死锁。

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

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

I assume this is either difficult or impossible for the compiler team to implement for some reason.

No, it is not at all difficult or impossible to implement -- the fact that you implemented it yourself is a testament to that fact. Rather, it is an incredibly bad idea and so we don't allow it, so as to protect you from making this mistake.

call to Monitor.Exit within ExitDisposable.Dispose seems to block indefinitely (most of the time) causing deadlocks as other threads attempt to acquire the lock. I suspect the unreliability of my work around and the reason await statements are not allowed in lock statement are somehow related.

Correct, you have discovered why we made it illegal. Awaiting inside a lock is a recipe for producing deadlocks.

I'm sure you can see why: arbitrary code runs between the time the await returns control to the caller and the method resumes. That arbitrary code could be taking out locks that produce lock ordering inversions, and therefore deadlocks.

Worse, the code could resume on another thread (in advanced scenarios; normally you pick up again on the thread that did the await, but not necessarily) in which case the unlock would be unlocking a lock on a different thread than the thread that took out the lock. Is that a good idea? No.

I note that it is also a "worst practice" to do a yield return inside a lock, for the same reason. It is legal to do so, but I wish we had made it illegal. We're not going to make the same mistake for "await".

美人骨 2024-12-14 20:31:03

使用 SemaphoreSlim.WaitAsync< /a> 方法。

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

Use the SemaphoreSlim.WaitAsync method.

 await mySemaphoreSlim.WaitAsync();
 try {
     await Stuff();
 } finally {
     mySemaphoreSlim.Release();
 }
听闻余生 2024-12-14 20:31:03

这只是 此答案,作者: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 建议的第四次修订)。

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

This is just an extension to this answer by user1639030.


Basic Version


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

Usage:

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

Extended Version


A version of the LockAsync method that claims to be completely deadlock-safe (from the 4th revision suggested by Jez).

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

Usage:

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. 
        });
    }
}
天涯沦落人 2024-12-14 20:31:03

基本上这是错误的做法。

有两种方法可以实现此目的:

  • 保持锁定,仅在块末尾释放它
    这是一个非常糟糕的主意,因为您不知道异步操作将花费多长时间。您应该只持有锁最短时间。这也可能是不可能的,因为线程拥有锁,而不是方法 - 并且您甚至可能无法在同一线程上执行异步方法的其余部分(取决于任务调度程序)。

  • 释放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)
{
}

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

Basically it would be the wrong thing to do.

There are two ways this could be implemented:

  • Keep hold of the lock, only releasing it at the end of the block.
    This is a really bad idea as you don't know how long the asynchronous operation is going to take. You should only hold locks for minimal amounts of time. It's also potentially impossible, as a thread owns a lock, not a method - and you may not even execute the rest of the asynchronous method on the same thread (depending on the task scheduler).

  • Release the lock in the await, and reacquire it when the await returns
    This violates the principle of least astonishment IMO, where the asynchronous method should behave as closely as possible like the equivalent synchronous code - unless you use Monitor.Wait in a lock block, you expect to own the lock for the duration of the block.

So basically there are two competing requirements here - you shouldn't be trying to do the first here, and if you want to take the second approach you can make the code much clearer by having two separated lock blocks separated by the await expression:

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

So by prohibiting you from awaiting in the lock block itself, the language is forcing you to think about what you really want to do, and making that choice clearer in the code that you write.

溺ぐ爱和你が 2024-12-14 20:31:03

这是指构建异步协调基元,第 6 部分: AsyncLock , http://winrtstoragehelper.codeplex.com/ 、Windows 8 应用商店和 .net 4.5

以下是我对此的看法:

async/await 语言功能使许多事情变得相当简单,但它也引入了一个场景那是
以前很少遇到这么容易使用异步调用:重入。

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

这是我在 Windows 8 应用商店应用程序中遇到的一个真实场景:
我的应用程序有两个框架:进入和离开一个框架,我想将一些数据加载/安全到文件/存储。
OnNavigedTo/From 事件用于保存和加载。保存和加载是通过一些异步实用函数完成的(例如 http://winrtstoragehelper.codeplex.com/ )。
当从帧 1 导航到帧 2 或沿其他方向导航时,将调用并等待异步加载和安全操作。
事件处理程序变为异步返回 void =>他们不能被等待。

但是,该实用程序的第一个文件打开操作(可以说:在保存函数内)也是异步的
因此,第一个等待将控制权返回给框架,该框架稍后通过第二个事件处理程序调用另一个实用程序(加载)。
负载现在尝试打开同一个文件,如果
文件现在已打开以进行保存操作,失败并出现 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/ 和我的修改版本,它使用 斯蒂芬·托布

我还可以推荐这个链接吗:
http://www.hanselman.com/blog/ComparingTwoTechniquesInNETAsynchronousCoordinationPrimitives.aspx

This referes to Building Async Coordination Primitives, Part 6: AsyncLock , http://winrtstoragehelper.codeplex.com/ , Windows 8 app store and .net 4.5

Here is my angle on this:

The async/await language feature makes many things fairly easy but it also introduces a scenario that was
rarely encounter before it was so easy to use async calls: reentrance.

This is especially true for event handlers, because for many events you don't have any clue about whats happening after you return from the event handler.
One thing that might actually happen is, that the async method you are awaiting in the first event handler, gets called from another event handler still on the
same thread.

Here is a real scenario I came across in a windows 8 App store app:
My app has two frames: coming into and leaving from a frame I want to load/safe some data to file/storage.
OnNavigatedTo/From events are used for the saving and loading. The saving and loading is done by some async utility function (like http://winrtstoragehelper.codeplex.com/).
When navigating from frame 1 to frame 2 or in the other direction, the async load and safe operations are called and awaited.
The event handlers become async returning void => they cant be awaited.

However, the first file open operation (lets says: inside a save function) of the utility is async too
and so the first await returns control to the framework, which sometime later calls the other utility (load) via the second event handler.
The load now tries to open the same file and if
the file is open by now for the save operation, fails with an ACCESSDENIED exception.

A minimum solution for me is to secure the file access via a using and an 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);
    }
}

Please note that his lock basically locks down all file operation for the utility with just one lock, which is unnecessarily strong but works fine for my scenario.

Here is my test project: a windows 8 app store app with some test calls for the original version from http://winrtstoragehelper.codeplex.com/ and my modified version that uses the AsyncLock from Stephen Toub.

May I also suggest this link:
http://www.hanselman.com/blog/ComparingTwoTechniquesInNETAsynchronousCoordinationPrimitives.aspx

跨年 2024-12-14 20:31:03

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

Stephen Taub has implemented a solution to this question, see Building Async Coordination Primitives, Part 7: AsyncReaderWriterLock.

Stephen Taub is highly regarded in the industry, so anything he writes is likely to be solid.

I won't reproduce the code that he posted on his blog, but I will show you how to use it:

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

If you want a method that's baked into the .NET framework, use SemaphoreSlim.WaitAsync instead. You won't get a reader/writer lock, but you will get tried and tested implementation.

蓝海似她心 2024-12-14 20:31:03

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

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

Hmm, looks ugly, seems to work.

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();
        }
    }
}
檐上三寸雪 2024-12-14 20:31:03

我创建了一个 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 效率低,并且分配更多资源,尽管它具有强制工作代码在完成后释放锁的好处(从技术上讲,返回的 IDisposableMutexAsyncable 无法通过调用代码释放并导致死锁)。它还具有额外的 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();
    }
}

I created a MutexAsyncable class, inspired by Stephen Toub's AsyncLock implementation (discussion at this blog post), which can be used as a drop-in replacement for a lock statement in either sync or async code:

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

It's safe to use the above if you're using .NET 5+ because that won't ever throw ThreadAbortException.

I also created an extended SemaphoreLocker class, inspired by this answer, which can be a general-purpose replacement for lock, usable either synchronously or asynchronously. It is less efficient than the above MutexAsyncable and allocates more resources, although it has the benefit of forcing the worker code to release the lock once it's finished (technically, the IDisposable returned by the MutexAsyncable could not get disposed by calling code and cause deadlock). It also has extra try/finally code to deal with the possibility of ThreadAbortException, so should be usable in earlier .NET versions:

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();
    }
}
神也荒唐 2024-12-14 20:31:03

我确实尝试使用 Monitor (下面的代码),它似乎可以工作,但有一个问题......当你有多个线程时,它会给出......

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

I did try using a Monitor (code below) which appears to work but has a GOTCHA... when you have multiple threads it will give...

System.Threading.SynchronizationLockException Object synchronization method was called from an unsynchronized block of code.

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

Prior to this I was simply doing this, but it was in an ASP.NET controller so it resulted in a deadlock.

public async Task<FooResponse> ModifyFooAsync()
{
    lock(lockObject)
    {
        return SomeFunctionToModifyFooAsync.Result;
    }
}
萝莉病 2024-12-14 20:31:03

我有自己的 AsyncLock 类。
我将其设为 IDisposable,因此它可以使用 using 块。
(idispose中信号量的解锁确保出现异常时它也会被解锁,而不需要try-finally)

namespace Test
{


    public abstract class AsyncLock
    : System.IAsyncDisposable
    {
        private readonly System.Threading.SemaphoreSlim m_semaphore;


        protected AsyncLock(System.Threading.SemaphoreSlim semaphore)
        {
            this.m_semaphore = semaphore;
        } // End Constructor 


        private class InternalAsyncSemaphoreSlimWrapper
            : AsyncLock
        {
            public InternalAsyncSemaphoreSlimWrapper(System.Threading.SemaphoreSlim semaphore)
               : base(semaphore)
            { } // End Constructor 

        } // End Class InternalAsyncSemaphoreSlimWrapper 


        private async System.Threading.Tasks.Task AcquireAsync()
        {
            await this.m_semaphore.WaitAsync();  // Asynchronously wait for the semaphore
        } // End Task AcquireAsync 


        public static async System.Threading.Tasks.Task<AsyncLock> LockAsync(System.Threading.SemaphoreSlim semaphore)
        {
            InternalAsyncSemaphoreSlimWrapper wrapper = new InternalAsyncSemaphoreSlimWrapper(semaphore);
            await wrapper.AcquireAsync();

            return wrapper;
        } // End Function LockAsync 


        async System.Threading.Tasks.ValueTask System.IAsyncDisposable.DisposeAsync()
        {
            this.m_semaphore.Release();  // Release the semaphore when disposed
            await System.Threading.Tasks.Task.CompletedTask;
        } // End Task DisposeAsync 


        internal static async System.Threading.Tasks.Task Test()
        {
            // private static readonly System.Threading.SemaphoreSlim s_consoleSemaphore = new System.Threading.SemaphoreSlim(1, 1);
            System.Threading.SemaphoreSlim s_consoleSemaphore = new System.Threading.SemaphoreSlim(1, 1);

            // because lock BLOCKS the thread when it waits 
            await using (AsyncLock consoleLocker = await AsyncLock.LockAsync(s_consoleSemaphore))
            {
                System.Console.WriteLine("inside the lock !");
            } // End Using consoleLocker 

        } // End Task Test 


    } // End Class AsyncLock 


} // End Namespace 

然后可以按如下方式使用

private static readonly System.Threading.SemaphoreSlim s_consoleSemaphore = 
  new System.Threading.SemaphoreSlim(1, 1);

internal static async System.Threading.Tasks.Task TestAsync()
{
    // no lock possible because lock BLOCKS the thread when it waits 
    // semaphore does not 
    await using (AsyncLock consoleLocker = await AsyncLock.LockAsync(s_consoleSemaphore))
    {
        await System.Console.Out.WriteLineAsync("inside the lock !");
    } // End Using consoleLocker 

} // End Task TestAsync 

如您所见,只需替换lock中的new object()与 new SemaphoreSlim(1, 1) 的

lock-语句进行比较:

private static readonly object s_consoleLock = new object();

internal static void Test()
{
    // no lock possible because lock BLOCKS the thread when it waits 
    // semaphore does not 
    lock(s_consoleLock)
    {
        System.Console.WriteLine("inside the lock !");
    } // End lock 

} // End Task Test 

I have my own class AsyncLock.
I made it IDisposable, so it can use a using-block.
(the unlock of the semaphore in the idispose ensures it will also be unlocked if there is an exception, without the need for try-finally)

namespace Test
{


    public abstract class AsyncLock
    : System.IAsyncDisposable
    {
        private readonly System.Threading.SemaphoreSlim m_semaphore;


        protected AsyncLock(System.Threading.SemaphoreSlim semaphore)
        {
            this.m_semaphore = semaphore;
        } // End Constructor 


        private class InternalAsyncSemaphoreSlimWrapper
            : AsyncLock
        {
            public InternalAsyncSemaphoreSlimWrapper(System.Threading.SemaphoreSlim semaphore)
               : base(semaphore)
            { } // End Constructor 

        } // End Class InternalAsyncSemaphoreSlimWrapper 


        private async System.Threading.Tasks.Task AcquireAsync()
        {
            await this.m_semaphore.WaitAsync();  // Asynchronously wait for the semaphore
        } // End Task AcquireAsync 


        public static async System.Threading.Tasks.Task<AsyncLock> LockAsync(System.Threading.SemaphoreSlim semaphore)
        {
            InternalAsyncSemaphoreSlimWrapper wrapper = new InternalAsyncSemaphoreSlimWrapper(semaphore);
            await wrapper.AcquireAsync();

            return wrapper;
        } // End Function LockAsync 


        async System.Threading.Tasks.ValueTask System.IAsyncDisposable.DisposeAsync()
        {
            this.m_semaphore.Release();  // Release the semaphore when disposed
            await System.Threading.Tasks.Task.CompletedTask;
        } // End Task DisposeAsync 


        internal static async System.Threading.Tasks.Task Test()
        {
            // private static readonly System.Threading.SemaphoreSlim s_consoleSemaphore = new System.Threading.SemaphoreSlim(1, 1);
            System.Threading.SemaphoreSlim s_consoleSemaphore = new System.Threading.SemaphoreSlim(1, 1);

            // because lock BLOCKS the thread when it waits 
            await using (AsyncLock consoleLocker = await AsyncLock.LockAsync(s_consoleSemaphore))
            {
                System.Console.WriteLine("inside the lock !");
            } // End Using consoleLocker 

        } // End Task Test 


    } // End Class AsyncLock 


} // End Namespace 

This can then be used as follows

private static readonly System.Threading.SemaphoreSlim s_consoleSemaphore = 
  new System.Threading.SemaphoreSlim(1, 1);

internal static async System.Threading.Tasks.Task TestAsync()
{
    // no lock possible because lock BLOCKS the thread when it waits 
    // semaphore does not 
    await using (AsyncLock consoleLocker = await AsyncLock.LockAsync(s_consoleSemaphore))
    {
        await System.Console.Out.WriteLineAsync("inside the lock !");
    } // End Using consoleLocker 

} // End Task TestAsync 

As you can see, new object() from lock is simply replaced with new SemaphoreSlim(1, 1)

The lock-statement for comparison:

private static readonly object s_consoleLock = new object();

internal static void Test()
{
    // no lock possible because lock BLOCKS the thread when it waits 
    // semaphore does not 
    lock(s_consoleLock)
    {
        System.Console.WriteLine("inside the lock !");
    } // End lock 

} // End Task Test 
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文