【问题标题】:SynchronizationLockException on Monitor.Exit when using await使用等待时 Monitor.Exit 上的 SynchronizationLockException
【发布时间】:2014-02-19 15:59:02
【问题描述】:

我正在创建一段代码,用于从我们拥有的旧系统中获取网页。为了避免过度查询,我将获取到的 URL 缓存起来。我正在使用Monitor.EnterMonitor.Exit 并仔细检查以避免发出两次请求,但是当使用Monitor.Exit 释放锁时,我得到了这个异常:

System.Threading.SynchronizationLockException was caught
  HResult=-2146233064
  Message=Object synchronization method was called from an unsynchronized block of code.
  Source=MyApp
  StackTrace:
       at MyApp.Data.ExProvider.<OpenFeature>d__0.MoveNext() in c:\Users\me\Documents\Visual Studio 2013\Projects\MyApp\MyApp\Data\ExProvider.cs:line 56
    --- End of stack trace from previous location where exception was thrown ---
       at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
       at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
       at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
       at MyApp.Data.ExProvider.<GetSupportFor>d__15.MoveNext() in c:\Users\me\Documents\Visual Studio 2013\Projects\MyApp\MyApp\Data\ExProvider.cs:line 71
  InnerException: 

第 56 行是Monitor.Exit。这是执行操作的代码:

private async Task<Stream> OpenReport(String report)
{
    var file = _directory.GetFiles(report+ ".html");
    if (file != null && file.Any())
        return file[0].OpenRead();
    else
    {
        try
        {
            Monitor.Enter(_locker);
            FileInfo newFile = new FileInfo(Path.Combine(_directory.FullName, report + ".html"));
            if (!newFile.Exists) // Double check
            {
                using (var target = newFile.OpenWrite())
                {
                    WebRequest request = WebRequest.Create(BuildUrl(report));
                    var response = await request.GetResponseAsync();
                    using (var source = response.GetResponseStream())
                        source.CopyTo(target);
                }
            }
            return newFile.OpenRead();
        }
        finally
        {
            Monitor.Exit(_locker);
        }
    }
}

那么awaitMonitor 有什么问题?是不是因为Monitor.EnterMonitor.Exit不是同一个线程?

【问题讨论】:

    标签: c# multithreading synchronization task-parallel-library async-await


    【解决方案1】:

    您不能在lock 范围内await 任务(这是Monitor.EnterMonitor.Exit 的语法糖)。直接使用Monitor 会欺骗编译器,但不会欺骗框架。

    async-await 没有像Monitor 那样的线程关联性。 await 之后的代码可能会在与之前的代码不同的线程中运行。这意味着释放Monitor 的线程不一定是获得它的线程。

    在这种情况下不要使用async-await,或者使用不同的同步结构,如SemaphoreSlimAsyncLock,您可以自己构建。这是我的:https://stackoverflow.com/a/21011273/885318

    【讨论】:

    • ManualResetEventSlim 应该可以吗?
    • 可以,但是将 SemaphoreSlim 设置为 1 会容易得多,因为您可以等待它并且它不是线程仿射的
    • 感谢您的解释。这很有意义。
    • 当我期待 SynchronizationLockException 时,我在以下代码块中没有收到任何异常。不确定为什么它没有按照已知的 await 和 lock 行为抛出异常。static void Main(string[] args) { AsyncFunctionWithLock(); } private static async void AsyncFunctionWithLock() { Monitor.Enter(lockObject); { await AsyncFunctionWithLock2(); } Monitor.Exit(lockObject); } private static async Task AsyncFunctionWithLock2() { }
    • @RasikBihariTiwari AsyncFunctionWithLock2 实际上并不是异步的。它不会等待任何东西,因此它会在同一个线程上同步继续。
    【解决方案2】:

    但是,在 SendRequest 中,我需要“等待”,因此由于某种原因我没有考虑太多,因此我无法使用锁定,因此同步的解决方案是使用 Monitor。

    应该多考虑一下。 :)

    async 代码中使用阻塞锁有两个问题。

    第一个问题是 - 在一般情况下 - async 方法可能会在不同的线程上恢复执行。大多数阻塞锁都是线程仿射的,这意味着它们必须从拥有它们的线程(获取锁的同一线程)中释放。正是这种对Monitor 线程关联性的违反导致了SynchronizationLockException。如果await 捕获执行上下文(例如,UI 上下文)并使用它来恢复async 方法(例如,在 UI 线程上),则不会发生此问题。或者,如果你很幸运,async 方法发生在同一个线程池线程上恢复。

    然而,即使你避免了第一个问题,你仍然有第二个问题:当async 方法在await 点“暂停”时,任何任意代码都可以执行。这违反了锁定的基本规则(“在持有锁定时不要执行任意代码”)。例如,线程仿射锁(包括Monitor)一般是可重入的,所以即使在UI线程场景下,当你的async方法“暂停”(并持有锁)时,其他方法在UI上运行线程可以毫无问题地获取锁。

    在 Windows Phone 8 上,请改用 SemaphoreSlim。这是一种允许阻塞和异步协调的类型。 Wait 用于阻塞锁,WaitAsync 用于异步锁。

    【讨论】:

    【解决方案3】:

    你可以使用互锁类来模拟lock语句,代码如下:

        private async Task<Stream> OpenReport(String report)
        {
            var file = _directory.GetFiles(report + ".html");
            if (file != null && file.Any())
                return file[0].OpenRead();
            else
            {
                object locker = _locker;
                try
                {
                    while (locker == null || Interlocked.CompareExchange(ref _locker, null, locker) != locker)
                    {
                        await Task.Delay(1);
                        locker = _locker;
                    }
                    FileInfo newFile = new FileInfo(Path.Combine(_directory.FullName, report + ".html"));
                    if (!newFile.Exists) // Double check
                    {
                        using (var target = newFile.OpenWrite())
                        {
                            WebRequest request = WebRequest.Create(BuildUrl(report));
                            var response = await request.GetResponseAsync();
                            using (var source = response.GetResponseStream())
                                source.CopyTo(target);
                        }
                    }
                    return newFile.OpenRead();
                }
                finally
                {
                    _locker = locker;
                }
            }
        }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-12-18
      • 1970-01-01
      • 1970-01-01
      • 2021-02-22
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多