首页 文章

为什么't I use the '可以在锁定语句的主体内等待'运算符?

提问于
浏览
268

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

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

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

8 回答

  • 5

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

    不,实现起来并不困难或不可能 - 你自己实施它的事实证明了这一事实 . 相反, it is an incredibly bad idea 因此我们不允许它,以保护您免于犯这个错误 .

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

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

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

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

    我注意到,出于同样的原因,在 lock 内进行 yield return 也是"worst practice" . 这样做是合法的,但我希望我们把它变成非法的 . 我们不会为"await"犯同样的错误 .

  • 212

    使用SemaphoreSlim.WaitAsync方法 .

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

    基本上这样做是错误的 .

    有两种方法可以实现:

    • Keep hold of the lock, only releasing it at the end of the block .
      这是一个非常糟糕的主意,因为你不知道异步操作需要多长时间 . 你应该只持有最少的时间锁 . 它也可能是不可能的,因为一个线程拥有一个锁,而不是一个方法 - 你甚至可能不会在同一个线程上执行其余的异步方法(取决于任务调度程序) .

    • Release the lock in the await, and reacquire it when the await returns
      这违反了最不惊讶的原则IMO,其中异步方法应该像等效同步代码一样尽可能地行事 - 除非你在锁定块中使用 Monitor.Wait ,否则你希望在块的持续时间内拥有锁 .

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

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

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

  • 14

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

    这是我对此的看法:

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

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

    这是我在Windows 8 App Store应用程序中遇到的一个真实场景:我的应用程序有两个框架:进入和离开框架我想要加载/保护一些数据到文件/存储 . OnNavigatedTo / 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);
        }
    }
    

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

    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

  • 60

    This is just an extension to 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();
            }
        }
    }
    

    Usage:

    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

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

  • 15

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

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

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

相关问题