431

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

来自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 语句的原因在某种程度上是相关的。

有谁知道为什么在 lock 语句的主体中不允许 await ?

4

8 回答 8

445

我认为编译器团队出于某种原因很难或不可能实现。

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

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

正确,您已经发现了我们将其设为非法的原因。在锁中等待是产生死锁的秘诀。

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

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

我注意到,出于同样的原因,做一个yield returninside a也是一种“最糟糕的做法” 。lock这样做是合法的,但我希望我们把它定为非法。我们不会对“等待”犯同样的错误。

于 2011-09-30T15:30:26.680 回答
366

使用SemaphoreSlim.WaitAsync方法。

 await mySemaphoreSlim.WaitAsync();
 try {
     await Stuff();
 } finally {
     mySemaphoreSlim.Release();
 }
于 2013-08-15T16:22:18.153 回答
77

基本上这是错误的做法。

有两种方法可以实现:

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

  • 释放等待中的锁,并在等待返回时重新获取它
    这违反了 IMO 最小惊讶原则,其中异步方法应尽可能像等效的同步代码一样表现 - 除非您Monitor.Wait在锁定块中使用,否则您希望在块的持续时间内拥有锁。

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

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

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

于 2011-09-30T15:29:48.310 回答
64

这只是这个答案的扩展。

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. 
        });
    }
}
于 2018-05-02T16:47:09.693 回答
18

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

于 2012-12-26T02:58:26.947 回答
9

Stephen Taub 已经为这个问题实现了一个解决方案,请参阅Building Async Coordination Primitives, Part 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改用。您不会获得读写器锁,但您将获得经过试验和测试的实现。

于 2014-09-12T08:18:37.880 回答
2

嗯,看起来很丑,似乎工作。

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();
        }
    }
}
于 2013-06-04T22:40:02.263 回答
-1

我确实尝试过使用监视器(下面的代码),它似乎可以工作,但有一个 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; } }

于 2017-09-01T10:15:32.337 回答