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

Kev*_*vin 318 .net c# async-await

锁定语句中不允许使用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();
}
Run Code Online (Sandbox Code Playgroud)

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

有谁知道为什么在锁定声明的正文中不允许等待?

Eri*_*ert 340

我认为编译器团队出于某种原因要么难以实施,要么难以实现.

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

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

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

我相信你可以理解为什么:在await将控制权返回给调用者并且方法恢复之间运行任意代码.任意代码可能会取出产生锁定顺序反转的锁,从而产生死锁.

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

我注意到,它也是一个"最差实践"做一个yield return内部的lock,出于同样的原因.这样做是合法的,但我希望我们把它变成非法的.我们不会为"等待"犯同样的错误.

  • 如何处理需要返回缓存条目的场景,如果该条目不存在,则需要异步计算内容然后添加+返回条目,确保在此期间没有其他人给您打电话? (165认同)
  • @Eric Lippert.鉴于在发布此答案之后,很好地将`SemaphoreSlim.WaitAsync`类添加到.NET框架中,我认为我们可以安全地假设现在可以使用它.无论如何,您对实施此类构造的难度的评论仍然完全有效. (9认同)
  • 我意识到我在这里参加派对已经很晚了,但是我很惊讶地发现你把死锁作为这个坏主意的主要原因.我在自己的想法中得出结论,锁/监视器的重入性质将成为问题的一个重要部分.也就是说,您将两个任务排队到线程池lock(),在同步世界中将在不同的线程上执行.但是现在等待(如果允许我的意思)你可以在锁定块中执行两个任务,因为线程被重用了.随之而来的是欢闹.还是我误解了什么? (8认同)
  • @ceztko:这是一个问答网站.我鼓励你提问作为问题,而不是评论. (7认同)
  • "任意代码在await将控制权返回给调用者并且方法恢复之间运行" - 在多线程上下文中,即使没有async/await,任何代码都是如此:其他线程可以在任何代码执行任意代码时间,并说你说的任意代码"可能会取出产生锁定顺序反转的锁,因此会出现死锁." 那么为什么这对async/await特别重要呢?我理解第二点是"代码可以在另一个线程上恢复"对async/await特别重要. (7认同)
  • @GarethWilson:我谈到了僵局*,因为问的问题是死锁*.你是正确的,奇怪的重新进入问题是可能的,似乎很可能. (4认同)
  • @Contango`SemaphoreSlim`仍然允许您执行锁反转(ouch),但是至少它没有线程亲和力和可重复性。至少这意味着您不在乎执行“阻塞”的线程。 (2认同)

小智 258

使用SemaphoreSlim.WaitAsync方法.

 await mySemaphoreSlim.WaitAsync();
 try {
     await Stuff();
 } finally {
     mySemaphoreSlim.Release();
 }
Run Code Online (Sandbox Code Playgroud)

  • 由于这种方法最近被引入到.NET框架中,我认为我们可以假设锁定async/await世界的概念现在已得到充分证明. (9认同)
  • 有关更多信息,请在本文中搜索文本"SemaphoreSlim":[Async/Await - 异步编程中的最佳实践](https://msdn.microsoft.com/en-us/magazine/jj991977.aspx) (5认同)
  • 添加了这个答案的扩展版本:/sf/answers/3509779311/ (4认同)
  • 它不应该被初始化为`mySemaphoreSlim = new SemaphoreSlim(1,1)`以便像`lock(...)`一样工作? (3认同)
  • @JamesKo 如果所有这些任务都在等待 `Stuff` 的结果,我看不出有任何解决办法...... (2认同)
  • @JamesKo 这就是使用信号量作为互斥锁的全部意义所在。只有一个任务应该执行 Stuff,因此所有其他任务都必须等待前一个任务释放锁并轮流一个一个地获取它。就像一堆人在厕所外面等着用它(只有一个厕所)。当他们对他们的朋友说“我要去厕所”时,并不意味着他们会立即回去。这意味着他们走了,等待所有其他人完成,然后使用厕所并回来。 (2认同)

Jon*_*eet 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)
{
}
Run Code Online (Sandbox Code Playgroud)

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

  • @Contango:那不是*相同的东西.特别是,信号量与特定线程无关.它实现了类似的锁定目标,但存在显着差异. (7认同)
  • 鉴于在发布此答案之后,很好地将`SemaphoreSlim.WaitAsync`类添加到.NET框架中,我认为我们可以安全地假设现在可以使用它.无论如何,您对实施此类构造的难度的评论仍然完全有效. (5认同)

Ser*_*gey 26

这只是这个答案的延伸.

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

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. 
        });
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 在“try”块之外获取信号量锁可能很危险 - 如果“WaitAsync”和“try”之间发生异常,信号量将永远不会被释放(死锁)。另一方面,将“WaitAsync”调用移动到“try”块中会引入另一个问题,即可以在没有获取锁的情况下释放信号量。请参阅解释此问题的相关线程:/sf/answers/4326472461/ (3认同)
  • @Nikolai 感谢您的参与!你是对的,但自从我稍微改变了我的技术堆栈以来,我已经一年多没有使用“async”/“await”了。顺便问一下,您对 AndreyCh 的评论有何看法?我真的没有时间去深入了解他的言论并对此发表任何言论。 (3认同)
  • @Jez我认为你的修改([第四次修订](https://stackoverflow.com/revisions/50139704/4))与OP的原始答案偏差太大。我建议将其作为单独的答案发布,以便可以根据其自身优点进行评估(投票)。 (2认同)

han*_*ans 16

这referes到http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx,http://winrtstoragehelper.codeplex.com/ ,Windows 8应用商店和.NET 4.5

这是我对此的看法:

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

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

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

但是,该实用程序的第一个文件打开操作(让我们说:在保存函数内)也是异步的,因此第一个await将控制权返回给框架,该框架稍后通过第二个事件处理程序调用另一个实用程序(加载).加载现在尝试打开同一个文件,如果文件现在已打开以进行保存操作,则会因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);
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,他的锁基本上只用一个锁来锁定该实用程序的所有文件操作,这是不必要的强,但对我的方案工作正常.

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

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


Con*_*ngo 6

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.
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您是一个融入.NET框架的方法,请SemaphoreSlim.WaitAsync改用.您将无法获得读取器/写入器锁定,但您将获得经过测试和测试的实现.