问题 对于异步方法,在发布模式下未调用IDisposable.Dispose()


我在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-loop运行一次,以指定产生问题的最小循环量。也可以随意使用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计划公开发布,但主分支中的最新版本应包含修复程序。

看到: https://github.com/dotnet/roslyn/issues/7669


5512
2017-12-22 18:09


起源

只是预感,但似乎Dispose()调用是在另一个线程上。它在调试中工作的原因是调试器正在为您切换线程。如果你添加了某种回调,让你回到正确的线程,你可能会有更好的运气。 - sapbucket
例如,查看代码如何使用Invoke所需: stackoverflow.com/questions/3874134/... - sapbucket
这似乎是一个错误。我可以在我的VS 2015中复制它。无论如何,它可能已经在最后一个版本中修复了(我没有最后一个版本)。如果错误仍然存​​在,你应该下载最后一个VS 2015,你最好现在让.NET团队(在中创建一个票证) github.com/dotnet/roslyn),因为发布/调试中的不同行为看起来非常难看。 - varocarbas
又一个可怕的罗斯林虫。这个是非常非常讨厌的方式,很容易根本不诊断。单击“新建问题”按钮 这个网页 报告错误。 - Hans Passant
问题创建: github.com/dotnet/roslyn/issues/7669 - Nick Muller


答案:


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

这个bug很难看,你必须用反编译器查看生成的程序集。我会以惊人的速度描述它。 Async Sub中的语句被重写为状态机,代码段中的特定类名是VB $ StateMachine_1_buttonClick。你只能用一个体面的反编译器来看它。该 MoveNext() 此类的方法执行方法体中的语句。异步代码运行时,会多次输入此方法。

MoveNext()使用的变量必须是 捕获,将您的局部变量转换为类的字段。像你的 window 变量,稍后当Using语句结束并且需要调用Dispose()方法时需要它。 Debug构建中此变量的名称是 $VB$ResumableLocal_window$0。当您构建程序的Release版本时,编译器会尝试优化此类并且错误地摸索。它 消除 捕获和制造 window MoveNext()的局部变量。这是非常错误的,当执行恢复后 Await,那个变量将是Nothing。因此不会调用它的Dispose()方法。

这个Roslyn错误具有非常大的影响,它会破坏任何使用该版本的VB.NET代码 Using 语句体包含Await的Async方法中的语句。这不容易诊断,丢失的Dispose()调用经常不被发现。除了像你这样的情况,它有一个非常明显的副作用。生产中必须有很多程序现在都有这个bug。副作用是它们会“重”,消耗的资源超过必要的资源。该程序可能以许多难以诊断的方式失败。

这个bug有一个临时的解决方法,一定要永远不要部署你的VB.NET应用程序的Debug版本,这有其他问题。改为关闭优化器。选择Release版本并使用Project> Properties> Compile选项卡> Advanced Compile Options>取消选中“Enable optimizations”复选框。

哎呀,这很糟糕。


12
2017-12-22 20:01



哇,听起来很难看。我真的很好奇为什么只有当await语句处于for-或while-loop时才会发生这种情况。我也相信罗斯林团队会喜欢你的分析! - Nick Muller
这非常难看。 For / While循环可能对bug有帮助,我没有深入挖掘它来分析它的副作用。可能会解释为什么以前没有找到这个bug。重写Async Sub的Roslyn代码非常重要,这些代码转换是编译器必须做的最复杂的事情。 - Hans Passant