【问题标题】:How to use async methods to load Db Data and keep UI responsive如何使用异步方法加载 Db 数据并保持 UI 响应
【发布时间】:2016-10-27 04:09:22
【问题描述】:

我制作了一个运行良好的大型应用程序,除了它的 UI(winforms)在使用 webclient 从 Web 检索数据时冻结(链接是嗯...不是最快的),或者从数据库检索查询的数据连接(它存储在一个又远又慢的服务器中——无法避免)。

所以我想利用异步方法,以便用户可以移动、最小化和单击窗口,而不会让操作系统感到紧张并将其标记为“无响应”。

我不希望我的代码在这些冗长的操作之间什么都不做,只是保持 UI 响应(我知道我应该禁用控件以防止用户在第一次操作完成之前询问其他内容或同样的问题) .

但是我对异步方法没有任何经验,我的第一次测试尝试是这样的:

Public Function GetDatatableUIblocked() As DataTable
    Dim retTable As New DataTable
    Dim connection As New OleDb.OleDbConnection("Provider=Microsoft.ACE.OLEDB.12.0;Data Source=\\lanserver\storage\DB.accdb;Persist Security Info=False;")
    Dim command = connection.CreateCommand()
    command.CommandText = "SELECT * FROM bdPROC;"
    connection.Open()
    Dim reader = command.ExecuteReader
    retTable.Load(reader)
    connection.Close()
    Return retTable
End Function

Public Function GetDatatableUIfree() As DataTable
    Dim retTable As New DataTable
    Dim connection As New OleDb.OleDbConnection("Provider=Microsoft.ACE.OLEDB.12.0;Data Source=\\lanserver\storage\DB.accdb;Persist Security Info=False;")
    Dim command = connection.CreateCommand()
    command.CommandText = "SELECT * FROM bdPROC;"
    connection.Open()
    Dim readerTask = command.ExecuteReaderAsync()

    readerTask.Start() '<=========================== EXCEPTION HAPPENS HERE

    Do
        Application.DoEvents()
    Loop Until readerTask.IsCompleted OrElse readerTask.IsFaulted
    Dim reader = readerTask.Result
    retTable.Load(reader)
    connection.Close()
    Return retTable
End Function

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    ProgressBar1.Visible = True
    Dim dt = GetDatatableUIblocked()
    ProgressBar1.Visible = False
    DataGridView1.DataSource = dt
End Sub

Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
    ProgressBar1.Visible = True
    Dim dt = GetDatatableUIfree()
    ProgressBar1.Visible = False
    DataGridView1.DataSource = dt
End Sub

但是,我在运行 readerTask.Start() 时遇到异常,它说“无法在已经完成的任务中调用 Start”(我正在翻译,我的 VS 不是英文的)。

我在 SO 中检查了一些线程,但老实说,我无法掌握这个概念并将其应用于我的问题,所以我谦虚地寻求帮助。非常感谢!

【问题讨论】:

  • 如果你评论那一行?
  • 圣洁线程,@Hackerman,就这么简单吗?异常消失了。但话又说回来,UI 在此期间仍然冻结,就像普通的顺序、同步运行一样。
  • 你一直在循环直到任务完成,所以你的 UI 仍然会被锁定
  • @ProGrammer - 循环内的Application.Doevents 不应该允许 UI 处理其鼠标单击和布局更改吗? (我有一个选框 ProgressBar 来提供 UI 正在工作的视觉标志)。
  • 据我了解,如果它们已经在您的线程被锁定时开始处理,它们将被处理。我不确定在最后一次调用Application.DoEvents()后触发事件时会受到怎样的影响@

标签: .net vb.net multithreading user-interface asynchronous


【解决方案1】:

让 UI 空闲的一种方法是使用 Task。所提出的解决方案在某种程度上阻碍了BackGroundWorker 的点,并使用无操作循环等待它完成。下面的代码还有一些值得注意的变化:

' note the Async modifier
Private Async Sub btnDoIt_Click(...
    Dim sql = "SELECT * FROM RandomData"
    dtSample = Await Task(Of DataTable).Run(Function() LoadDataTable(sql))
    dgv2.DataSource = dtSample
End Sub

Private Function LoadDataTable(sql As String) As DataTable
    ' NO UI/Control references allowed
    Dim dt = New DataTable
    Using dbcon As New OleDbConnection(ACEConnStr)
        Using cmd As New OleDbCommand(sql, dbcon)
            dbcon.Open()
            dt.Load(cmd.ExecuteReader())
        End Using
    End Using
    Return dt
End Function

我没有 OP 中提到的确切条件,但我确实有一个包含 500k 行的 Access 表。这对 OleDB 造成了足够的负担,以至于加载可能需要 6-10 秒,这足以判断 UI 是否保持响应。确实如此。

  1. 不要忘记在等待Task 完成的任何方法上使用Asynch
  2. LoadTable 方法设置为从有效查询中加载任何表,因此其他事物可以使用它。如果/当查询更改时,只需更改调用它的内容即可。
  3. 当您完成释放资源并防止泄漏时,应该释放 DB Provider 对象,例如 Connections 和 DBCommand 对象。 Using 块为我们完成了这项工作。
  4. 不需要 do-nothing/DoEvents 循环,也不需要完成通知的事件。 Await 代码将等待加载方法完成,以便之后可以完成您需要做的任何其他事情。

Task 通常比BackGroundWorker 更易于使用。

资源

【讨论】:

  • 谢谢,我将您的代码调整为适合我的代码,但为了使其正常工作,我必须递归地将自 button.click 处理程序以来的所有方法声明标记为异步,并将所有方法调用标记为等待。据我了解这个概念,这正是必须要做的。但是我可能很难在我的应用程序中实现它,这个应用程序有点大并且已经投入生产了一段时间。因此,我可能更喜欢 BGW 解决方案,尽管它可能很狡猾,而且只是在预期延迟时间长到足以惹恼用户的地方。不过,我认为您的回答是正确的。
  • 重构 GetDataTable 方法的一部分是使其可重用,并说明可以通过向其传递 SQL 来加载任何表。也就是说,您可以让一个代表其他人完成工作,而不是分散在 10-12 个 Async 方法上。
【解决方案2】:

没关系。我用 BackgroundWorker 得到了我想要的东西:

Public Function GetDatatableUIfree() As DataTable
    Dim connection As New OleDb.OleDbConnection("Provider=Microsoft.ACE.OLEDB.12.0;Data Source=\\lanserver\storage\DB.accdb;Persist Security Info=False;")
    Dim command = connection.CreateCommand()
    command.CommandText = "SELECT * FROM bdPROC;"
    connection.Open()
    Dim retTable As New DataTable, bgw As New BackgroundWorker, bgw_complete As Boolean
    AddHandler bgw.DoWork,
        Sub(sender As Object, e As DoWorkEventArgs) retTable.Load(command.ExecuteReader)
    AddHandler bgw.RunWorkerCompleted,
        Sub(sender As Object, e As RunWorkerCompletedEventArgs) bgw_complete = True
    bgw.RunWorkerAsync()
    Do
        Application.DoEvents()
    Loop Until bgw_complete
    connection.Close()
    Return retTable
End Function

非常感谢您的帮助。

【讨论】:

  • 您不需要使用 BGW 的等待循环。 RunWorkerCompleted 会告诉您何时完成
  • @Plutonix,是的,但是因为我想保留 Function GetDatatableUIfree() 中包含的命令序列,而不是将其分解为 (1) 起始子和 (2) 事件处理程序子,是否存在还有其他方法吗?特别是因为我想将它封装在我的“数据获取”方法中,这样我就不需要在请求数据查询的那么多地方更改代码。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-01-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-05-03
  • 2018-04-08
  • 1970-01-01
相关资源
最近更新 更多