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:
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:
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With