【问题标题】:IDisposable.Dispose() not called in Release mode for async methodIDisposable.Dispose() 未在异步方法的发布模式下调用
【发布时间】:2016-03-29 01:53:54
【问题描述】:

我在 VS2015.1 上使用 .NET 4.6.1 在 VB.NET 14 中编写了以下 WPF 示例应用程序:

Class MainWindow

    Public Sub New()
        InitializeComponent()
    End Sub

    Private Async Sub Button_Click(sender As Object, e As RoutedEventArgs)
        MessageBox.Show("Pre")

        Using window = New DisposableWindow()
            window.Show()

            For index = 1 To 1
                Await Task.Delay(100)
            Next
        End Using

        MessageBox.Show("Post")
    End Sub

    Class DisposableWindow
        Inherits Window
        Implements IDisposable

        Public Sub Dispose() Implements IDisposable.Dispose
            Me.Close()
            MessageBox.Show("Disposed")
        End Sub
    End Class

End Class

下面的示例产生以下输出:

  • 调试模式:Pre、Disposed、Post
  • 发布模式:Pre、Post

这很奇怪。为什么 Debug 模式执行此代码的方式与 Release 模式不同...?

当我将 using 块更改为手动 try/finally 块时,对 window.Dispose() 的调用甚至会引发 NullReferenceException:

Dim window = New DisposableWindow()
Try
    window.Show()

    For index = 1 To 1
        Await Task.Delay(100)
    Next
Finally
    window.Dispose()
End Try

还有更奇怪的东西:当排除 for 循环时,示例可以完美运行。我只让 For 循环运行一次,以指定产生问题的最小循环量。也可以随意用 While 循环替换 For 循环。它产生与 For 循环相同的行为。

作品:

Using window = New DisposableWindow()
    window.Show()

    Await Task.Delay(100)
End Using

现在您可能会想:“这很奇怪!”。它变得更糟。 我还在 C# (6) 中制作了完全相同的示例,它可以完美运行。因此,在 C# 中,Debug 和 Release 模式都会导致 'Pre, Disposed, Post' 作为输出。

样本可以在这里下载:

http://www.filedropper.com/vbsample

http://www.filedropper.com/cssample

在这一点上我很困惑。这是 .NET Framework 的 VB.NET 堆栈中的错误吗?还是我试图完成一些奇怪的事情,幸运的是,这似乎是 C# 中的工作,部分是 VB.NET 中的工作?

编辑:

做了一些测试:

  • 在 VB.NET 中为发布模式禁用编译器优化,使其行为类似于调试模式(如预期的那样,但想对其进行测试,以防万一)。
  • 当我以 .NET 4.5(async/await 可用的最早版本)为目标时,也会出现此问题。

更新:

此问题已得到修复。 1.2 版计划公开发布,但 master 分支中的最新版本应该包含修复。

见:https://github.com/dotnet/roslyn/issues/7669

【问题讨论】:

  • 只是一种预感,但似乎 Dispose() 调用在不同的线程上。它在调试中起作用的原因是调试器正在为您切换线程。如果您添加了某种回调,将您切换回正确的线程,您可能会有更好的运气。
  • 例如看代码如何使用Invoke required:stackoverflow.com/questions/3874134/…
  • 这似乎是一个错误。我可以在我的 VS 2015 中复制它。无论如何,它可能已经在最新版本中修复(我没有最新版本)。如果错误仍然存​​在,您应该下载最后一个 VS 2015,您最好现在让 .NET 团队(在 github.com/dotnet/roslyn 创建票证),因为发布/调试中的不同行为看起来很丑。
  • 又一个可怕的 Roslyn 错误。这是一个非常非常讨厌的方法,很容易根本不诊断。点击this web page上的“新问题”按钮报告错误。

标签: .net vb.net async-await


【解决方案1】:

我会写这个,这个 Roslyn 错误非常讨厌,可能会破坏很多 VB.NET 程序。以一种非常丑陋且难以诊断的方式。

这个错误很难发现,你必须用反编译器查看生成的程序集。我将以惊人的速度描述它。 Async Sub 中的语句被重写为状态机,sn-p 中的特定类名是 VB$StateMachine_1_buttonClick。您只能使用像样的反编译器才能看到它。该类的MoveNext() 方法执行方法体中的语句。当你的异步代码运行时,这个方法被多次输入。

MoveNext() 使用的变量需要捕获,将您的局部变量转换为类的字段。就像您的 window 变量一样,稍后在 Using 语句结束并且需要调用 Dispose() 方法时将需要它。 Debug 版本中此变量的名称是 $VB$ResumableLocal_window$0。当您构建程序的 Release 版本时,编译器会尝试优化此类并且会失败。它消除捕获并使window 成为MoveNext() 的局部变量。这是非常错误的,当在Await 之后继续执行时,该变量将为Nothing。因此不会调用它的 Dispose() 方法。

这个 Roslyn 错误影响非常大,它会破坏在 Async 方法中使用 Using 语句的任何 VB.NET 代码,其中语句主体包含 Await。这不容易诊断,丢失的 Dispose() 调用经常未被检测到。除非在像您这样的情况下,它具有非常明显的副作用。现在必须有很多在生产中运行的程序存在此错误。副作用是它们会运行“沉重”,消耗比必要更多的资源。该程序可能会以许多难以诊断的方式失败。

此错误有一个临时解决方法,请确保永远不要部署 VB.NET 应用程序的调试版本,它有其他问题。而是关闭优化器。选择 Release build and use Project > Properties > Compile tab > Advanced Compile Options > 取消勾选“Enable optimizations”复选框。

哎呀,这很糟糕。

【讨论】:

  • 哇,听起来很难看。我真的很好奇为什么只有在等待语句处于 for 或 while 循环时才会发生这种情况。我也相信 Roslyn 团队会喜欢你的分析!
  • 非常难看。 For/While 循环很可能对 bug 有帮助,但我没有深入挖掘来分析它的副作用。可能会解释为什么以前没有发现这个错误。重写 Async Sub 的 Roslyn 代码非常重要,这种代码转换是编译器必须做的最复杂的事情。
猜你喜欢
  • 2016-12-03
  • 1970-01-01
  • 2015-03-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多