Как правильно закрыть Winform при выполнении асинхронной задачи?

(Проверьте конец вопроса в разделе Edit2, где опубликован окончательный рабочий код из принятого ответа)

Я создаю приложение Winforms (.Net 7), которое отображает статистику на графиках. Источником статистических данных является запрос MySQL, который выполняется в Async Task. После завершения запроса результат datatable привязывается к диаграмме.

Когда приложение запускается, появляется основная Winform, где есть кнопки, которые запускают разные Winform или несколько экземпляров одной и той же Winform.

Моя проблема в том, что если, например, в одном из дочерних Winforms выполняется асинхронный запрос MySQL, и я закрываю этот Winform с помощью «x», и теперь на экране находится только основная Winform, то при завершении запроса возникает исключение System.NullReferenceException: 'Object reference not set to an instance of an object.' когда таблица данных привязана к диаграмме. Очевидно, закрытия Winform недостаточно для освобождения всех связанных с ним ресурсов (я не имею в виду уничтожение запроса, который, естественно, продолжает выполняться на сервере MySQL). Я ожидал, что если я закрою форму, то с ней все будет покончено.

Каков правильный подход в таких ситуациях? Как я могу избавиться от исключений из Winform, который был закрыт, пока его задача не была завершена? Наличие таких исключений влияет на взаимодействие с пользователем, когда пользователи работают в других формах того же приложения.

Упрощенный пример моего кода приведен ниже:

Public Class Form4
    Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

        Dim TabCell As New DataTable
        'This is the statistical table

        Dim getStats As New Queries
        '"Queries " is a class containing query execution functions

        TabCell = Await Task().Run(Function() getStats.ExecQuery(parameters))
        'This is the asynchronous run of the task for query execution.
        'If the Winform is closed while this task is running...
        '...there is the mentioned exception in the line below:
        Chart1.DataSource = TabCell

        Chart1.Series.Clear()
        Chart1.Titles.Clear()
        'Further code for chart definition

    End Sub
End Class

Код класса Queries приведен ниже.

Public Class Queries

    Public Property connStr As String

    Public Function ExecQuery(date1 As Date, date2 As Date, queryString As String) As DataTable
        Dim dTab As New DataTable
        Dim SQL As New MySqlCommand
        Dim Adap As New MySqlDataAdapter

        Try
            MysqlConn.ConnectionString = connStr
            MysqlConn.Open()

            SQL.Parameters.AddWithValue("@date1", date1)
            SQL.Parameters.AddWithValue("@date2", date2)

            SQL.Connection = MysqlConn
            SQL.CommandText = queryString
            SQL.CommandTimeout = 1200

            Adap.SelectCommand = SQL
            Adap.Fill(dTab)

        Catch Err As MySqlException
            MsgBox("Error: " & Err.Number & " - " & Err.Message)
        Finally
            MysqlConn.Close()
            MysqlConn.Dispose()
        End Try

        Return dTab

    End Function

End Class

Обновлено: я публикую здесь код из ответа, полученного от пользователя «Zch», но не знаю, почему он бесследно исчез. Этот ответ был очень хорошо структурирован и по большей части полезен. Там объяснялось, как бороться с CancellationToken.

Я реализовал это, и это сработало как шарм. Теперь, когда Winform закрывается до завершения Async Task, исключений больше нет.

Часть, которую я не могу понять, как заставить это работать, — это реализация CancellationToken в функции выполнения запроса в классе Queries. Должен сказать, что код в Winform работает вполне нормально без записей, касающихся токена в классе Queries. Пожалуйста, проверьте ниже точные строки, для которых я не понимаю, для чего они предназначены.

If token.IsCancellationRequested Then
    token.ThrowIfCancellationRequested()
End If

Предполагается ли, что эта запись уничтожит запрос на стороне сервера MySQL? Автор ответа написал следующее:

Убедитесь, что ваш query func выдержит отмену, предоставив a CancelationToken и периодически проверяем его.

Я не знаю, как это периодически проверять.

Полный код из исчезнувшего ответа показан ниже. Это модификация моего кода с добавлением записей для CancelationToken.

Public Class Form4
    Private cts As CancellationTokenSource

    Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        cts = New CancellationTokenSource()
        Dim token As CancellationToken = cts.Token

        Try
            Dim TabCell As New DataTable
            'store the data from the query

            Dim getStats As New Queries
            'runs the queries

            TabCell = Await Task.Run(Function() getStats.ExecQuery(parameters, token), token)
            'Run the async query with the cancellation token

            If Not Me.IsDisposed Then
                Chart1.DataSource = TabCell
                Chart1.Series.Clear()
                Chart1.Titles.Clear()
                'Set up your data chart
            End If
        Catch ex As OperationCanceledException
            'The task was canceled, yay
        Catch ex As Exception
            'Handle other exceptions
        Finally
            cts.Dispose()
        End Try
    End Sub
    
    Private Sub Form4_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing
        If cts IsNot Nothing Then
            cts.Cancel()
        End If
    End Sub
End Class

Public Class Queries
    Public Function ExecQuery(parameters As Object, token As CancellationToken) As DataTable
        Dim dt As New DataTable

        'Simulate a long-running query
        For i As Integer = 1 To 10
            If token.IsCancellationRequested Then
                token.ThrowIfCancellationRequested()
            End If
            Threading.Thread.Sleep(420)
        Next

        'Your query logic goes here
        'Populate the DataTable

        Return dt
    End Function
End Class

Edit2: Ниже вы можете найти окончательный рабочий код из принятого ответа. Есть некоторые отличия от кода в разделе «Редактирование» относительно использования функциональности Adapter.FillAsync соединителя MySQL, а также добавления cts = Nothing после End Try и удаления cts.Dispose() в разделе Finally.

Public Class Form4
    Private cts As CancellationTokenSource

    Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    
        cts = New CancellationTokenSource()
        Dim token As CancellationToken = cts.Token

        Try
            Dim TabCell As New DataTable
            'store the data from the query

            Dim getStats As New Queries
            'runs the queries

            TabCell = Await Task.Run(Function() getStats.ExecQuery(queryString, token), token)
            'Run the async query with the cancellation token

            If Not Me.IsDisposed Then
                Chart1.DataSource = TabCell
                Chart1.Series.Clear()
                Chart1.Titles.Clear()
                'Set up your data chart
            End If
            
        Catch ex As OperationCanceledException
            'The task was canceled, yay
        Catch ex As Exception
            'Handle other exceptions
        End Try
        
        cts = Nothing
        
    End Sub
    
    Private Sub Form4_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing
        If cts IsNot Nothing Then
            cts.Cancel()
        End If
    End Sub
End Class

Public Class Queries
    Public Async Function ExecQuery(queryString As String, token As CancellationToken) As Task(Of DataTable)
        Dim dt As New DataTable
        Dim SQL As New MySqlCommand
        Dim Adap As New MySqlDataAdapter

        Try

            SQL.Connection = MysqlConn
            SQL.CommandText = queryString

            Adap.SelectCommand = SQL
            Await Adap.FillAsync(dTab, token)

            token.ThrowIfCancellationRequested()

        Catch Err As MySqlException
            MsgBox("Error: " & Err.Number & " - " & Err.Message)
        End Try

        Return dt
    End Function
End Class

Проверьте Learn.microsoft.com/en-us/dotnet/api/…

Steve 29.05.2024 23:23

Вам следует использовать доступные асинхронные методы, передавая CancellationToken. Затем вы можете вызвать [CancellationTokenSource].Cancel(), когда форма вот-вот закроется (в OnFormClosing/OnFormClosed)

Jimi 30.05.2024 01:29

@Jimi Я добавил новую информацию в вопрос (раздел «Редактировать»), которая взята из ответа, который я получил здесь, но, вероятно, был удален по какой-то причине. Есть хорошая реализация CancellationToken. Но я не понимаю, как правильно передать токен в функцию выполнения запроса MySQL.

Ivaylo 30.05.2024 18:42

@Jimi Я не совсем понимаю, что вы имели в виду, говоря об использовании асинхронных методов в документации MySQL Connector. Это то, что должно заменить Await Async при запуске Task для выполнения функции запроса MySQL? Я имею в виду следующее: TabCell = Await Task().Run(Function() getStats.ExecQuery(parameters)). Я спрашиваю, потому что в документах я вижу следующее await proc.ExecuteNonQueryAsync(). Предполагается ли, что оба подхода работают вместе, или мне просто нужно использовать асинхронные методы соединителя MySQL (которые я не предпочитаю, поскольку слишком много кода нужно изменить и снова протестировать)?

Ivaylo 30.05.2024 18:48

Я имею в виду именно это: используйте асинхронные методы, предоставляемые библиотекой. Так, например, await [Connection].OpenAsync([CancellationToken]), await [DataAdapter].FillAsync([...],[CancellationToken]), await [Command].ExecuteNonQueryAsync([CancellationToken]) и так далее. В исходном коде особо нечего менять, вам нужно дождаться этих методов, а не просто запускать их синхронно. Конечно, передавая каждому один и тот же CancellationToken, чтобы каждый из них можно было отменить, когда это станет необходимым, независимо от того, где находится текущая процедура, когда запрашивается отмена.

Jimi 30.05.2024 20:53

@Jimi Должен ли я ожидать, что передача CancellationToken в await [DataAdapter].FillAsync([...],[CancellationToken]) уничтожит выполняющийся запрос на сервере MySQL? Я пока не нашел никаких гарантий, что это возможно. Если это невозможно, то мне следует запустить FillAsync вместо просто Fill, когда я могу полагаться только на асинхронный вызов метода выполнения запроса (чтобы не блокировать пользовательский интерфейс) и на отмену в Winform (чтобы не было исключения) и забыть, что запрос все еще выполняется, поскольку моему приложению он больше не нужен?

Ivaylo 30.05.2024 22:56

@Джими, возможно, я не совсем понимаю функцию CancellationToken в моем случае. Если я удалю все асинхронные методы MySQL (например, FillAsync, все токены и события закрытия формы в последнем исходном коде в моем вопросе и оставлю только If Not Me.IsDisposed Then непосредственно перед кодом определения диаграммы, тогда исключений больше не будет. Так какой цели служит CancellationToken, поскольку он не убивает запрос на стороне сервера?

Ivaylo 30.05.2024 23:22

CancellationToken предназначен для совместной отмены задачи, поэтому ее текущие операции прекращаются. Что касается запроса к базе данных, это означает, что задействована и серверная часть (реализация влияет на инфраструктуру, если все сделано правильно). Отмена может быть сигнализирована в виде исключения. Следовательно, вы можете прервать любую операцию, независимо от того, на каком этапе она находится. У вас есть сомнения относительно реальной функциональности реализации MySQL? Затем протестируйте его и посмотрите, что произойдет, если вы отмените операцию, которая может (или не может) требовать ответа на стороне сервера.

Jimi 31.05.2024 05:11

Ваша текущая реализация в ExecQuery() бессмысленна, а проверка IsDisposed приводит к состоянию гонки, которое вы не можете контролировать. Плюс куча других движущихся частей, которые могут мешать.

Jimi 31.05.2024 05:12
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
9
154
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Он исчез бесследно, потому что был удален модом (Привет @Da***a o/ ) и помечен как сгенерированный ИИ, и я разозлился на письма, сгенерированные ИИ, у меня нет представителя, чтобы оставлять комментарии, и я не буду даже заморачиваюсь с примерами кода, но суть такова:

При работе с CancellationToken цель состоит в том, чтобы позволить корректно отменять задачи, особенно. длительные задачи, такие как запросы к базе данных.

Проверьте отмену!

Линии:

If token.IsCancellationRequested Then
 token.ThrowIfCancellationRequested()

ОБВ. проверьте, была ли запрошена отмена -> Если да -> поставьте OperationCanceledException, который остановит выполнение задачи. В моем случае я разместил такие проверки в различных точках, для вас я бы поместил их в ваши долго выполняющиеся задачи, чтобы гарантировать, что задачу можно будет остановить как можно скорее, если форма закроется.

Несколько дополнительных замечаний!

Локальное выполнение: эти проверки помогают остановить локальное выполнение задачи в вашем приложении. например если пользователь закроет форму, задача перестанет выполняться локально.

Серверная сторона: это не останавливает запрос непосредственно на сервере MySQL. Как я уже сказал, это только останавливает выполнение задачи в вашем приложении.

Периодические проверки: рискуя быть обвиненными в том, что я ИИ, я повторяюсь: чтобы беспрепятственно использовать CancellationToken, вставьте проверки в очевидном месте. точки в вашей функции, например, в циклах или после длительных операций внутри самой задачи. У меня сработало...

Ивайло, твои подозрения верны:

CancellationToken не уничтожает запрос на стороне сервера. Это останавливает локальную задачу в вашем приложении. Установив token.IsCancellationRequested, вы можете остановить длительные операции локально, если форма закрыта, что предотвращает возникновение исключений.

Использование асинхронных методов, таких как OpenAsync, FillAsync и ExecuteNonQueryAsync, с токенами отмены помогает корректно обрабатывать эти операции и сохранять отзывчивость пользовательского интерфейса. Благодаря реализации периодических проверок и использованию этих асинхронных методов ваше приложение должно корректно обрабатывать отмену задач. Вуаля, мы оптимизировали, предотвратили исключения и получили более плавный UX.

Имхо, речь идет об управлении задачей внутри приложения, а не о непосредственном уничтожении процесса на стороне сервера. Эта часть, как отметил @Jimi, обрабатывается вышеупомянутыми асинхронными методами.

Надеюсь это поможет!

(С нетерпением жду бана от разгневанного хорватского модера - или, может быть, она смилостивится в 2024 году? Кстати @Da***a Я не знаю, какие методы обнаружения вы, ребята, используете, но просто чтобы доказать свою точку зрения... gbNuiQj.png @ i .imgur.com – счастливого бана/удаления НХФ)

StackOverflow теряет трафик не только из-за ChatGpt и т.п. сервисов, но и из-за бесполезной модерации и кислых админов. Мои вопросы привлекают все меньше и меньше внимания, так зачем еще спрашивать здесь? Плюсом является огромная база данных вопросов по истории.

Ivaylo 31.05.2024 10:18

Для других пользователей, дошедших до этого момента, вы можете найти код ответа в разделе «Редактировать» вопроса. Исходный ответ с кодом был удален.

Ivaylo 31.05.2024 10:30

Кстати, нет необходимости выполнять проверки отмены if с помощью token.ThrowIfCancellationRequested(), этот метод выполняет эту проверку в своем теле. Название метода подразумевает такое поведение: ThrowIf...

Ryan 01.06.2024 04:30

ТИ, я спотыкался в темноте, когда мне пришлось, по крайней мере, стараться изо всех сил, чтобы начать возвращать услугу тому, кто был рядом со мной, когда я потерял все и был в самом худшем состоянии, скорбя о многочисленных смертях, прикованный к постели из-за болезни и в отчаянии - один и без дохода, и после начала выздоровления внезапно пришлось писать код после долгого периода, когда я не касался ничего более сложного, чем случайные несколько строк bat/ps/vbs/bash, черт возьми, после нескольких лет болезни я боролся даже с АХК, спасибо за подсказку, теперь она довольно очевидна, когда вы на это указываете, я чувствую себя отстойным :P <3

Zch 01.06.2024 05:40

@Zch, ахаха, это не так уж и плохо, эта ошибка просто добавляет несколько битов тактовой частоты процессора. И я рад, сэр, помочь вам сделать код немного чище и выбраться из этой тьмы. " = "

Ryan 01.06.2024 06:10

@Zch Я думаю, что в вашем решении должна быть одна модификация в блоке try catch finally end try. Finally часть следует удалить и после End Try должна остаться cts = Nothing. В противном случае при обычном закрытии формы после завершения всех асинхронных процессов cts удаляется, а в form closing event возникает исключение, которое cts удаляется при попытке закрыть ее.

Ivaylo 02.06.2024 23:58

@Ivaylo Итак, если я правильно вас понял, добавление cts.Dispose и установка cts в Nothing после Try/End Try, как вы сказали (при периодической проверке отмены в классе Queries и выдаче, если это запрошено), CancellationTokenSource правильно удаляется, а закрытие формы обрабатывается изящно, используя асинхронные методы из MySQL Connector, как упомянул @Jimi, и передайте CancellationToken . Не могу протестировать прямо сейчас, мой процессор загружен на 99% (не спрашивайте: P [Совет профессионала: не будьте мной и не перезагружайте Windows более 4 месяцев])

Zch 16.06.2024 23:29

@Zch Идею установки cts на Nothing я взял с сайта Microsoft - вы можете проверить там два последних примера с исходными кодами. В своем вопросе я разместил как Edit2 последнее обновление кода с учетом всех предложений. Он работает нормально и никаких проблем. Я не могу периодически проверять отмену в классе Queries, потому что это один длинный запрос. Таким образом, проверка выполняется, как только она уже завершена.

Ivaylo 18.06.2024 00:31

Другие вопросы по теме