IDisposable.Dispose() не вызывается в режиме Release для асинхронного метода
Я написал следующее примерное приложение WPF в VB.NET 14, используя .NET 4.6.1 на 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
Пример ниже дает следующий результат:
- Режим отладки: Pre, Disposed, Post
- Режим выпуска: Pre, Post
Это странно. Почему режим Debug выполняет этот код иначе, чем режим Release...?
Когда я меняю блок использования на ручной блок 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-loop исключено, образец работает отлично. Я разрешаю только цикл For-loop, чтобы указать минимальное количество циклов, которые создают проблему. Также не стесняйтесь заменять For-loop на цикл While. Он производит то же поведение, что и For-loop.
Работы:
Using window = New DisposableWindow()
window.Show()
Await Task.Delay(100)
End Using
Теперь вы можете подумать: "Это странно!". Это становится еще хуже.
Я также сделал тот же пример в С# (6), где он отлично работает. Таким образом, в С# оба режима Debug и Release приводят к выводу "Pre, Disposed, Post" в качестве вывода.
Сэмплы можно скачать здесь:
http://www.filedropper.com/vbsample
http://www.filedropper.com/cssample
На данный момент я очень взволнован. Является ли это ошибкой в стеке VB.NET.NET Framework? Или я пытаюсь выполнить что-то странное, что по счастью кажется работой на С# и частично в VB.NET?
Edit:
Проделал еще несколько тестов:
- Отключение оптимизаций компилятора в режиме VB.NET для выпуска, приводит к тому, что он ведет себя как режим отладки (как и ожидалось, но хотел проверить его на всякий случай).
- Проблема также возникает, когда я нацелен на .NET 4.5 (самая ранняя версия, где async/await стал доступен).
Update:
С тех пор исправлено. Публичный релиз запланирован на версию 1.2, но последняя версия в главной ветке должна содержать исправление.
Смотрите: https://github.com/dotnet/roslyn/issues/7669
Ответы
Ответ 1
Я напишу об этом, эта ошибка Roslyn чрезвычайно неприятна и может сломать множество программ VB.NET. В очень уродливой и трудно диагностируемой форме.
Ошибка довольно трудно увидеть, вам нужно посмотреть на сгенерированную сборку с декомпилятором. Я опишу его на скорости разлома. Операторы в Async Sub переписываются в конечный автомат, а конкретное имя класса в вашем фрагменте - VB $StateMachine_1_buttonClick. Вы можете видеть его только с достойным декомпилятором. Метод MoveNext()
этого класса выполняет инструкции в теле метода. Этот метод вводится несколько раз, пока выполняется ваш асинхронный код.
Переменные, используемые MoveNext(), необходимо захватить, превратив ваши локальные переменные в поля класса. Как и ваша переменная window
, она понадобится позже, когда заканчивается оператор Using, и нужно вызвать метод Dispose(). Имя этой переменной в сборке Debug равно $VB$ResumableLocal_window$0
. Когда вы создаете сборку Release своей программы, компилятор пытается оптимизировать этот класс и плохо работает. Он устраняет захват и делает window
локальной переменной MoveNext(). Это ужасно неправильно, когда выполнение возобновляется после Await
, эта переменная будет Nothing. И поэтому его метод Dispose() не будет вызываться.
Эта ошибка Roslyn имеет очень большой эффект afaict, она сломает любой код VB.NET, который использует оператор Using
в методе Async, где тело оператора содержит Await. Это нелегко диагностировать, отсутствующий вызов Dispose() очень часто остается незамеченным. За исключением случая, подобного вашему, где он имеет очень видимый побочный эффект. Должно быть много запущенных программ, которые имеют эту ошибку прямо сейчас. Побочным эффектом является то, что они будут работать "тяжелыми", потребляя больше ресурсов, чем необходимо. Программа может терпеть неудачу во многих трудных целях диагностики.
Существует временный обходной путь для этой ошибки, не забудьте никогда не развертывать сборку Debug вашего приложения VB.NET, у которого есть другие проблемы. Вместо этого выключите оптимизатор. Выберите сборку выпуска и используйте "Проект" > "Свойства" > вкладка "Компиляция" > "Дополнительные параметры компиляции" > установите флажок "Включить оптимизацию".
Yikes, это плохо.