为什么我不能在lock语句的主体中使用'await'运算符?

时间:2011-09-30 15:23:01

标签: c# .net async-await

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

来自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的调用似乎无限期地(大部分时间)阻塞,导致死锁,因为其他线程试图获取锁。我怀疑我的工作不可靠以及锁定语句中不允许等待语句的原因在某种程度上是相关的。

有人知道为什么 await在锁定声明的正文中是不允许的?

8 个答案:

答案 0 :(得分:320)

  

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

不,实施起来并不困难或不可能 - 你自己实施它的事实证明了这一事实。相反,这是一个非常糟糕的主意,所以我们不允许它,以免你犯这个错误。

  

调用ExitDisposable.Dispose中的Monitor.Exit似乎无限期地(大部分时间)阻塞,导致死锁,因为其他线程试图获取锁。我怀疑我的工作不可靠以及锁定语句中不允许等待语句的原因在某种程度上是相关的。

正确,你已经发现我们为什么把它变成非法的。 在锁内等待是产生死锁的一种方法。

我确定你可以看到原因:任意代码在await将控制权返回给调用者并且方法恢复之间运行。任意代码可能会取出产生锁定顺序反转的锁,从而产生死锁。

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

我注意到,出于同样的原因,在yield return内进行lock也是“最糟糕的做法”。这样做是合法的,但我希望我们把它变成非法的。我们不会因为“等待”而犯同样的错误。

答案 1 :(得分:236)

使用SemaphoreSlim.WaitAsync方法。

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

答案 2 :(得分:64)

基本上这样做是错误的。

可以通过以下两种方式实现

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

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

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

答案 3 :(得分:18)

这只是this answer的扩展。

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

<强>用法:

public class Test
{
    private static readonly SemaphoreLocker _locker = new SemaphoreLocker();

    public async Task DoTest()
    {
        await _locker.LockAsync(async () =>
        {
            // [asyn] calls can be used within this block 
            // to handle a resource by one thread. 
        });
    }
}

答案 4 :(得分:15)

这引用了http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspxhttp://winrtstoragehelper.codeplex.com/,Windows 8应用商店和.net 4.5

以下是我对此的看法:

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

对于事件处理程序尤其如此,因为对于许多事件,您不了解从事件处理程序返回后发生的事情。 实际上可能发生的一件事是,你在第一个事件处理程序中等待的异步方法,从另一个仍然在 同一个帖子。

这是我在Windows 8 App Store应用中遇到的一个真实场景: 我的应用程序有两个框架:进入和离开框架我想加载/保护一些数据到文件/存储。 OnNavigatedTo / From事件用于保存和加载。保存和加载由一些异步实用程序功能(如http://winrtstoragehelper.codeplex.com/)完成。 当从第1帧导航到第2帧或在另一个方向上导航时,将调用并等待异步加载和安全操作。 事件处理程序变为异步返回void =&gt;他们无法等待。

然而,该实用程序的第一个文件打开操作(让我们说:在保存函数内)也是异步的 所以第一个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);
    }
}

请注意,他的锁基本上只用一个锁来锁定该实用程序的所有文件操作,这是不必要的强大但适用于我的场景。

Here是我的测试项目:一个Windows 8应用程序商店应用程序,其中包含对来自http://winrtstoragehelper.codeplex.com/的原始版本的一些测试调用以及使用Stephen Toub http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx的AsyncLock的修改版本。

我可以建议这个链接: http://www.hanselman.com/blog/ComparingTwoTechniquesInNETAsynchronousCoordinationPrimitives.aspx

答案 5 :(得分:6)

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

答案 6 :(得分:4)

嗯,看起来很丑,似乎有效。

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

答案 7 :(得分: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; } }