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


348

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

MSDN

等待表达式不能用于同步函数,查询表达式,异常处理语句的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语句的原因与某种原因有关。

有谁知道为什么在锁语句的主体内不允许等待?


27
我以为您发现了不允许这样做的原因。
asawyer 2011年


我刚刚开始追赶并了解有关异步编程的更多信息。在我的wpf应用程序中无数死锁之后,我发现本文是异步编程实践中的绝佳安全防护。msdn.microsoft.com/en-us/magazine/...
C. Tewalt

当异步访问会破坏您的代码时,锁的目的是防止异步访问,如果在锁内使用异步,则会使锁无效。因此,如果您需要等待锁内的某些东西,则说明您未正确使用锁
MikeT

Answers:


366

我认为由于某种原因,对于编译器团队而言,这既困难又不可能。

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

在DisposeDisposable.Dispose中对Monitor.Exit的调用似乎无限期地(大部分时间)阻塞,导致死锁,因为其他线程试图获取该锁。我怀疑我的工作不可靠,而锁定语句中不允许使用await语句的原因与某种原因有关。

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

我敢肯定,您会明白为什么:在await将控制权返回给调用方与方法恢复之间的这段时间内,任意代码都会运行。那个任意代码可能会取出产生锁顺序倒置的锁,从而导致死锁。

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

我注意到,出于相同的原因,对a进行yield return内部操作也是“最差的做法” lock。这样做是合法的,但我希望我们将其定为非法。我们不会为“等待”犯同样的错误。


188
您如何处理需要返回缓存条目的情况,如果该条目不存在,则需要异步计算内容,然后添加并返回该条目,以确保在此期间没有其他人调用您?
Softlion

9
我意识到我在这里参加晚会很晚,但是我很惊讶地看到您将僵局作为这不是一个好主意的主要原因。我以自己的想法得出结论,即锁/监控器的可重入性将是问题的更大部分。也就是说,您将两个任务排队到lock()线程池中,这两个任务在同步世界中将在单独的线程上执行。但是现在有了await(如果允许的话),您可以在锁块中执行两个任务,因为线程被重用了。随之而来的是狂欢。还是我误会了什么?
加雷斯·威尔逊

4
@GarethWilson:我说的是死锁,因为问题是死锁。您是正确的,有可能并且似乎有可能发生奇怪的重入问题。
埃里克·利珀特

11
@埃里克·利珀特 鉴于SemaphoreSlim.WaitAsync在您发布此答案之后,该类就已经很好地添加到了.NET框架中,我认为我们可以放心地假设现在是可能的。无论如何,您对实现这种构造的难度的评论仍然完全有效。
Contango 2014年

7
“任意代码在await将控制权返回给调用者和方法恢复之间运行。”-在多线程上下文中,即使没有async / await,任何代码也肯定如此:其他线程可以在任何位置执行任意代码时间,然后说出任意代码,就像您所说的那样:“可能会取出产生锁顺序倒置的锁,从而导致死锁。” 那么,为什么这对于异步/等待特别重要?我理解第二点关于“代码可以在另一个线程上恢复”对于异步/等待特别重要。
bacar

291

使用SemaphoreSlim.WaitAsync方法。

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

10
随着这种方法最近被引入.NET框架,我认为我们可以假设锁定异步/等待世界的概念现已得到充分证明。
Contango 2014年

5
有关更多信息,请在本文中搜索文本“ SemaphoreSlim”:Async /
Await-

1
@JamesKo,如果所有这些任务都在等待结果,Stuff我看不到任何解决方法……
Ohad Schneider

7
不应该将其初始化为mySemaphoreSlim = new SemaphoreSlim(1, 1)像这样工作lock(...)吗?
谢尔盖

3
添加了此答案的扩展版本:stackoverflow.com/a/50139704/1844247
Sergey,

67

基本上,这将是错误的事情。

有两个方面,这可以实现:

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

  • 释放等待中的锁,然后在等待返回时重新获取锁。
    这违反了IMO的最低要求原则,即异步方法的行为应与等效的同步代码尽可能接近-除非您Monitor.Wait在锁块中使用,否则您希望在该区块的持续时间内拥有该锁。

因此,这里基本上有两个相互竞争的需求-您不应该在这里尝试第一个,而如果您想采用第二种方法,则可以通过用await表达式分隔两个分开的锁块来使代码更清晰:

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

因此,通过禁止您等待锁块本身,该语言迫使您考虑自己真正想做的事情,并在编写的代码中使选择更清晰。


5
鉴于SemaphoreSlim.WaitAsync在您发布此答案之后,该类就已经很好地添加到了.NET框架中,我认为我们可以放心地假设现在是可能的。无论如何,您对实现这种构造的难度的评论仍然完全有效。
Contango 2014年

7
@Contango:嗯,那不是完全一样的东西。特别是,信号量不与特定线程绑定。它实现了相似的锁定目标,但是存在很大差异。
乔恩·斯基特

@JonSkeet我知道这是一个非常老的线程,但是我不确定第二种方式使用这些锁如何保护something()调用?当线程正在执行something()时,任何其他线程也可以参与其中!我在这里想念什么吗?

@Joseph:当时没有受到保护。这是第二种方法,它可以清楚地说明您正在获取/释放,然后可能在其他线程上再次获取/释放。因为按照埃里克的回答,第一种方法是个坏主意。
乔恩·斯基特

41

这只是该答案的扩展。

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

1
将信号量锁锁定在try块外可能很危险-如果在两者之间发生异常WaitAsync并且try信号量将永远不会释放(死锁)。另一方面,将WaitAsync调用移到该try块中会带来另一个问题,即可以在不获取锁的情况下释放信号量。请参阅解释该问题的相关线程:stackoverflow.com/a/61806749/7889645
AndreyCh

16

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

这是我的看法:

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

对于事件处理程序尤其如此,因为对于许多事件,从事件处理程序返回后,您对发生的事情一无所知。实际上可能发生的一件事是,您在第一个事件处理程序中等待的async方法是从仍在同一线程上的另一个事件处理程序调用的。

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

但是,该实用程序的第一个文件打开操作(在保存函数中说:)也是异步的,因此第一个await将控制权返回给框架,该控件稍后有时会通过第二个事件处理程序调用另一个实用程序(加载)。加载现在尝试打开同一文件,并且如果该文件现在已打开以进行保存操作,则加载失败,并出现ACCESSDENIED异常。

对我来说,最小的解决方案是通过使用和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/的原始版本,以及经过修改的版本,该版本使用来自Stephen Toub http://blogs.msdn的AsyncLock 。 com / b / pfxteam / archive / 2012/02/12 / 10266988.aspx

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


7

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改用。您不会获得读取器/写入器锁,但是将获得经过实践检验的实现。


我很好奇,知道是否有使用此代码的注意事项。如果有人能证明此代码有任何问题,我想知道。但是,真正的事实是,SemaphoreSlim.WaitAsync与.NET框架一样,异步/等待锁定的概念也得到了充分证明。这些代码所做的只是添加一个读取器/写入器锁定概念。
Contango 2014年

3

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

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

0

我确实尝试使用似乎可以工作但具有GOTCHA的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; } }

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.