VB Visual Basic 2026 中文教程
第 32 课 · 让 Windows Forms 保持响应

Async/Await 异步编程

当程序读取数据库、调用网络 API 或等待其他 I/O 操作时,如果所有工作都同步阻塞 UI 线程,窗体可能暂时无法移动、按钮无法响应,看起来像“卡住”。Async/Await 让程序等待耗时操作时把控制权交回 UI 消息循环,操作完成后再继续执行。本课从 Task.Delay 入门,并进一步把学生数据库读取改成真正的异步数据访问。

环境:Visual Studio 2026 · VB.NET · .NET 10数据库:Microsoft.Data.SqlClient实作:StudentAsyncLoader2026

本课学习目标

  • 理解同步执行与异步执行的区别。
  • 理解为什么长时间阻塞 UI 线程会让窗体失去响应。
  • 认识 TaskTask(Of T)
  • 使用 Async 声明异步方法。
  • 使用 Await 等待 Task。
  • 使用 Task.Delay() 模拟异步等待。
  • 理解为什么一般方法应优先使用 Async Function 而不是 Async Sub。
  • 在 Windows Forms Click 事件中使用 Async Sub
  • 使用 SqlConnection.OpenAsync()
  • 使用 ExecuteReaderAsync()ReadAsync()
  • 完成 StudentAsyncLoader2026。

32.1 同步与异步有什么不同?

同步等待

当前方法一直占着执行流程等待结果。若发生在 UI 线程,窗体可能无法及时处理重绘、移动和点击消息。

异步等待

遇到尚未完成的 Await 时,方法可以暂时让出控制;Task 完成后,再从 Await 后面继续。

32.2 为什么 Windows Forms 会“卡住”?

例如:

Private Sub btnStart_Click(
    sender As Object,
    e As EventArgs) Handles btnStart.Click

    Threading.Thread.Sleep(5000)

    lblStatus.Text =
        "完成"

End Sub

Thread.Sleep 会让执行这个事件的线程睡眠 5 秒。如果这是 UI 线程,期间窗体无法正常处理其他 UI 工作。

不要把 Thread.Sleep 当成异步等待 Sleep 是阻塞线程;Task.Delay 返回 Task,可以配合 Await 实现非阻塞等待。

32.3 第一个 Async/Await 示例

Private Async Sub btnStart_Click(
    sender As Object,
    e As EventArgs) Handles btnStart.Click

    lblStatus.Text =
        "处理中..."

    Await Task.Delay(5000)

    lblStatus.Text =
        "完成"

End Sub

等待的 5 秒内,窗体仍然可以继续处理 UI 消息。

32.4 Await 时发生了什么?

Click进入事件
Async 方法开始执行
AwaitTask 未完成
UI 可继续响应等待期间
继续执行Task 完成

32.5 什么是 Task?

Task 表示一个可能尚未完成的异步操作。

Task
代表异步工作,但完成后没有业务返回值。
Task(Of Integer)
完成后返回 Integer。
Task(Of String)
完成后返回 String。
Task(Of List(Of Student))
完成后返回学生列表。

32.6 Async Function 返回 Task

Private Async Function WaitForMessageAsync() As Task

    Await Task.Delay(2000)

End Function

调用:

Await WaitForMessageAsync()

32.7 Async Function 返回 Task(Of T)

Private Async Function GetMessageAsync() As Task(Of String)

    Await Task.Delay(1000)

    Return "异步操作完成。"

End Function

调用:

Dim message As String =
    Await GetMessageAsync()

lblStatus.Text =
    message

32.8 为什么 Async Sub 要谨慎使用?

Windows Forms 事件处理器的签名本来就是 Sub,因此可以写:

Private Async Sub btnLoad_Click(
    sender As Object,
    e As EventArgs) Handles btnLoad.Click

但一般可等待的方法,更建议:

Private Async Function LoadDataAsync() As Task
简单规则 事件处理器可以用 Async Sub;自己编写的一般异步操作,优先让它返回 Task 或 Task(Of T),这样调用方可以 Await,也更容易处理错误。

32.9 Await 会取得异步结果

如果方法返回:

Task(Of Integer)

那么:

Dim count As Integer =
    Await GetStudentCountAsync()

Await 完成后,count 直接得到 Integer 结果。

32.10 Async/Await 与异常处理

Await 可以继续配合熟悉的 Try...Catch:

Try

    Dim students =
        Await LoadStudentsAsync()

Catch ex As Exception

    MessageBox.Show(
        ex.Message)

End Try

异步操作失败时,异常会在 Await 处重新表现出来,因此可以用正常的 Try...Catch 处理。

32.11 异步操作期间禁用按钮

btnLoad.Enabled =
    False

Try

    Await LoadDataAsync()

Finally

    btnLoad.Enabled =
        True

End Try

这样可以防止用户在同一操作尚未完成时重复点击。

32.12 把 Repository 改成异步读取

建立:

Public Async Function GetAllAsync() _
    As Task(Of List(Of Student))

完整方法:

Public Async Function GetAllAsync() _
    As Task(Of List(Of Student))

    Const sql As String =
        "SELECT StudentID, Name, Course, Score " &
        "FROM dbo.Students " &
        "ORDER BY StudentID;"

    Dim students As New List(Of Student)()

    Using connection As New SqlConnection(
        DatabaseHelper.ConnectionString)

        Using command As New SqlCommand(
            sql,
            connection)

            Await connection.OpenAsync()

            Using reader As SqlDataReader =
                Await command.ExecuteReaderAsync()

                While Await reader.ReadAsync()

                    students.Add(
                        New Student(
                            reader.GetInt32(0),
                            reader.GetString(1),
                            reader.GetString(2),
                            reader.GetDecimal(3)))

                End While

            End Using

        End Using

    End Using

    Return students

End Function

32.13 OpenAsync()

Await connection.OpenAsync()

它是打开 SQL Server 连接的异步版本。程序不需要在等待数据库连接建立期间阻塞当前调用流程。

32.14 ExecuteReaderAsync()

Using reader As SqlDataReader =
    Await command.ExecuteReaderAsync()

这会异步执行 SELECT,并在完成后得到 SqlDataReader。

32.15 ReadAsync()

While Await reader.ReadAsync()

    ' 读取当前记录

End While

ReadAsync 是 Read 的异步版本,用来前进到下一笔记录。

32.16 Service 也返回 Task

Public Class StudentService

    Private ReadOnly repository As StudentRepository

    Public Sub New(
        repository As StudentRepository)

        Me.repository =
            repository

    End Sub

    Public Async Function GetStudentsAsync() _
        As Task(Of List(Of Student))

        Return Await repository.GetAllAsync()

    End Function

End Class

32.17 Form 异步加载数据

Private Async Function LoadStudentsAsync() As Task

    Dim students As List(Of Student) =
        Await studentService.GetStudentsAsync()

    dgvStudents.DataSource =
        Nothing

    dgvStudents.DataSource =
        students

    lblStatus.Text =
        $"已加载 {students.Count} 位学生。"

End Function

32.18 异步加载按钮

Private Async Sub btnLoad_Click(
    sender As Object,
    e As EventArgs) Handles btnLoad.Click

    btnLoad.Enabled =
        False

    lblStatus.Text =
        "正在读取数据库..."

    Try

        Await LoadStudentsAsync()

    Catch ex As SqlException

        MessageBox.Show(
            ex.Message,
            "数据库错误",
            MessageBoxButtons.OK,
            MessageBoxIcon.Error)

        lblStatus.Text =
            "读取失败。"

    Finally

        btnLoad.Enabled =
            True

    End Try

End Sub

32.19 实作:Student Async Loader 2026

建立:

StudentAsyncLoader2026
Student Async Loader 2026
模拟等待:
2000 ms
数据库:
StudentManagementDB
异步加载学生 模拟异步工作 清除
Student ID姓名课程成绩
1李明Visual Basic 202688.00
3陈伟Python Programming91.00
状态:异步读取完成,UI 保持响应。

32.20 用 Task.Delay 观察 UI 响应

Private Async Sub btnSimulate_Click(
    sender As Object,
    e As EventArgs) Handles btnSimulate.Click

    btnSimulate.Enabled =
        False

    lblStatus.Text =
        "模拟耗时工作..."

    Try

        Await Task.Delay(3000)

        lblStatus.Text =
            "模拟工作完成。"

    Finally

        btnSimulate.Enabled =
            True

    End Try

End Sub

运行后,在等待期间尝试移动窗体,你会发现窗体仍能响应。

32.21 不要把所有工作都丢给 Task.Run

对于本来就提供异步 API 的 I/O 操作,例如:

OpenAsync()
ExecuteReaderAsync()
ReadAsync()

优先直接 Await 它们,而不是:

Await Task.Run(
    Sub()
        connection.Open()
    End Sub)
Async 不等于“开一个新线程” I/O 异步的重点是等待期间不阻塞调用线程,并不意味着每个 Await 都会启动新的线程。

32.22 I/O-bound 与 CPU-bound

I/O-bound

数据库、网络、文件等待。优先使用 API 自带的 Async 方法并 Await。

CPU-bound

大量计算、图像处理等真正持续占用 CPU 的工作,才可能考虑 Task.Run 把计算移出 UI 线程。

32.23 一个 Async 方法可以有多个 Await

Private Async Function DemoAsync() As Task

    lblStatus.Text =
        "步骤 1"

    Await Task.Delay(1000)

    lblStatus.Text =
        "步骤 2"

    Await Task.Delay(1000)

    lblStatus.Text =
        "完成"

End Function
开始 Await 1 继续 Await 2 完成

32.24 Task.WhenAll 简介

如果几个异步操作彼此独立,可以先启动,再一起等待:

Dim task1 As Task =
    Task.Delay(1500)

Dim task2 As Task =
    Task.Delay(2000)

Await Task.WhenAll(
    task1,
    task2)

这为以后同时调用多个 Web API 或并行等待多个 I/O 任务打下基础。

32.25 Async Sub 的异常为什么更难管理?

因为 Async Sub 不返回 Task,调用者无法这样写:

Await SomeAsyncSub()

因此一般逻辑应写成:

Private Async Function SaveAsync() As Task

而事件处理器只是最外层入口:

Private Async Sub btnSave_Click(...) _
    Handles btnSave.Click

    Await SaveAsync()

End Sub

32.26 初学者常见错误

错误 1

方法里有 Await 却没有 Async

包含 Await 的方法需要使用 Async 修饰符。

错误 2

一般方法全部写 Async Sub

除事件处理器外,通常应返回 Task 或 Task(Of T)。

错误 3

用 Thread.Sleep 模拟异步

Sleep 阻塞线程;Task.Delay 才适合本课的异步等待演示。

错误 4

忘记 Await

调用 Async 方法却不 Await,后续代码可能在操作尚未完成时就继续。

32.27 异步数据库读取流程

1
用户点击 Load

事件处理器是 Async Sub。

2
Await Service

Form 等待 Task(Of List(Of Student))。

3
OpenAsync

Repository 异步建立数据库连接。

4
ExecuteReaderAsync / ReadAsync

异步等待数据库结果。

5
回到 Form 更新 UI

绑定 DataGridView 并恢复按钮。

32.28 小练习

  1. 把 Task.Delay 从 3 秒改成 8 秒,观察窗体是否仍能移动。
  2. 写一个 GetStudentCountAsync(),返回 Task(Of Integer)
  3. 把 Repository 的 INSERT 改成 ExecuteNonQueryAsync()
  4. 新增“异步新增学生”按钮,并在操作期间禁用按钮。
  5. 使用 Try...Catch 捕获异步数据库异常。
  6. 建立两个 Task.Delay,并用 Task.WhenAll 等待。
  7. 解释为什么数据库 Async API 不需要再包在 Task.Run 中。

本课复习

  1. 同步等待为什么可能冻结 Windows Forms UI?
  2. Task 表示什么?
  3. Task(Of T) 与 Task 有什么不同?
  4. Async 的作用是什么?
  5. Await 的作用是什么?
  6. 为什么 Task.Delay 比 Thread.Sleep 更适合异步等待?
  7. 为什么事件处理器可以用 Async Sub,但一般方法优先 Async Function?
  8. OpenAsync() 做什么?
  9. ExecuteReaderAsync()ReadAsync() 分别做什么?
  10. I/O-bound 与 CPU-bound 有什么基本区别?
技术参考: 本课异步语法和数据库异步 API 按 Microsoft Learn 当前文档整理。 Visual Basic Async/Await · Await Operator · SqlConnection.OpenAsync