11

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

Nick Muller
  • 1,399
  • 11
  • 32
  • just a hunch, but it appears that the Dispose() call is on a different thread. The reason it works in debug is that the debugger is switching threads for you. If you added some sort of callback that switched you back to the correct thread you might have better luck. – sapbucket Dec 22 '15 at 18:59
  • For example, see how the code uses Invoke required: http://stackoverflow.com/questions/3874134/cleaning-up-code-littered-with-invokerequired – sapbucket Dec 22 '15 at 19:01
  • @sapbucket: Sounds reasonable, but that doesn't explain why it works in C#. Both languages should compile to about the same MSIL, right? Wait, that gives me an idea: I should compare the MSIL. I'll try that later! – Nick Muller Dec 22 '15 at 19:01
  • 1
    It does seem a bug. I am able to replicate it in my VS 2015. In any case, it might have already been fixed in the last version (I don't have the last version). You should download the last VS 2015 if the error persists, you should better let the .NET team now (create a ticket in https://github.com/dotnet/roslyn), because a different behaviour in release/debug looks quite ugly. – varocarbas Dec 22 '15 at 19:02
  • @varocarbas: I have VS2015.1. That should be the latest version. But I was thinking the same. I'll let the MS team know! – Nick Muller Dec 22 '15 at 19:03
  • @sapbucket It is irrelevant which behaviour should be expected (it might even trigger an error); both debug and release modes should always deliver exactly the same. – varocarbas Dec 22 '15 at 19:03
  • 2
    Yet another horrible Roslyn bug. This one is very, very nasty, way to easy to not diagnose this at all. Click the "New Issue" button on [this web page](https://github.com/dotnet/roslyn/issues) to report the bug. – Hans Passant Dec 22 '15 at 19:17
  • 3
    Issue created: https://github.com/dotnet/roslyn/issues/7669 – Nick Muller Dec 22 '15 at 19:23
  • And fixed. Updated the question with relevant information. – Nick Muller Dec 30 '15 at 03:37

1 Answers1

12

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.

Hans Passant
  • 873,011
  • 131
  • 1,552
  • 2,371
  • Wow, that sounds ugly. I'm really curious why this only happens if the await statement is in a for- or while-loop. I'm also sure the Roslyn team would love your analysis! – Nick Muller Dec 22 '15 at 20:15
  • 2
    It is very ugly. The For/While loop might well be instrumental in the bug, I did not dig in deep enough to analyze its side-effects. Might explain why the bug hasn't been found before. The Roslyn code that rewrites your Async Sub is highly non-trivial, these kind of code transformations are the most complicated thing a compiler has to do. – Hans Passant Dec 22 '15 at 20:18