Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

IDisposable.Dispose() not called in Release mode for async method

I wrote the following WPF sample app in VB.NET 14 using .NET 4.6.1 on VS2015.1:

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

The sample below produces the following output:

  • Debug mode: Pre, Disposed, Post
  • Release mode: Pre, Post

This is strange. Why would Debug mode execute this code differently than Release mode...?

When I change the using block to a manual try/finally block, the call to window.Dispose() even throws a NullReferenceException:

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

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

And even more strange stuff: When the for-loop is excluded, the sample works perfectly. I've only let the For-loop run once, to specify the minimum amount of loops the produce the issue. Also feel free to replace the For-loop with a While-loop. It produces the same behavior as the For-loop.

Works:

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

    Await Task.Delay(100)
End Using

Now you might think: 'That is strange!'. It gets even worse. I've also made the exact same example in C# (6), where it works perfectly. So in C# both Debug and Release mode result in 'Pre, Disposed, Post' as output.

The samples can be downloaded here:

http://www.filedropper.com/vbsample

http://www.filedropper.com/cssample

I'm pretty stumped at this point. Is this a bug in the VB.NET stack of .NET Framework? Or am I trying to accomplish something strange, which by luck seems the work in C# and partially in VB.NET?

Edit:

Did some more test:

  • Disabling compiler optimizations in VB.NET for Release mode, makes it behave like Debug mode (as expected, but wanted to test it, just in case).
  • The issue also happens when I target .NET 4.5 (the earliest version where async/await became available).

Update:

This has since been fixed. Public release is planned for version 1.2, but the latest version in the master branch should contain the fix.

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

like image 585
Nicky Muller Avatar asked Dec 22 '15 18:12

Nicky Muller


1 Answers

I'll write this one up, this Roslyn bug is exceedingly nasty and liable to break a lot of VB.NET programs. In a very ugly and difficult to diagnose way.

The bug is pretty hard to see, you have to look at the generated assembly with a decompiler. I'll describe it at break-neck speed. The statements in the Async Sub get rewritten into a state machine, the specific class name in your snippet is VB$StateMachine_1_buttonClick. You can only see it with a decent decompiler. The MoveNext() method of this class executes the statements in the method body. This method is entered multiple times while your async code runs.

Variables used by MoveNext() need to be captured, turning your local variables into fields of the class. Like your window variable, it will be needed later when the Using statement ends and the Dispose() method needs to be called. The name of this variable in the Debug build is $VB$ResumableLocal_window$0. When you build the Release build of your program, the compiler attempts to optimize this class and fumbles badly. It eliminates the capture and makes window a local variable of MoveNext(). This is horribly wrong, when execution resumes after the Await, that variable will be Nothing. And thus its Dispose() method won't be called.

This Roslyn bug has a very large impact afaict, it will break any VB.NET code that uses the Using statement in an Async method where the statement body contains an Await. This is not easy to diagnose, a missing Dispose() call very often goes undetected. Except in a case like yours where it has a very visible side-effect. There must be lots programs running in production that have this bug right now. Side-effect is that they'll run "heavy", consuming more resources than necessary. The program can fail in many hard to diagnose ways.

There is a temporary workaround for this bug, be sure to never deploy the Debug build of your VB.NET app, that has other problems. Turn off the optimizer instead. Select the Release build and use Project > Properties > Compile tab > Advanced Compile Options > untick the "Enable optimizations" checkbox.

Yikes, this is bad.

like image 187
Hans Passant Avatar answered Oct 05 '22 23:10

Hans Passant