异步程序中的控制流(C# 和 Visual Basic)

您可以使用 Async 和 Await 关键字更轻松地编写和维护异步程序。 但是,如果您不清楚程序的运行状况,结果可能会出乎意料。 本主题介绍了简单异步程序中的控制流,向你演示控制何时从一个方法移动到另一个方法以及每次转移了什么信息。

备注

在 Visual Studio 2012 中引入 Async 和 Await 关键字。

一般情况下,您可以使用 Async (Visual Basic)async (C#) 修饰符标记包含异步代码的方法。 在标有 Async 修饰符的方法中,可以使用等待 (Visual Basic)等待 (c#) 运算符指定该方法在何处停止以等待完成调用的异步过程。 有关详细信息,请参阅 使用 Async 和 Await 的异步编程(C# 和 Visual Basic)

下面的示例使用异步方法下载指定网站的内容作为字符串并显示该字符串的长度。 该示例包含下面两种方法。

  • startButton_Click,调用 AccessTheWebAsync 并显示结果。

  • AccessTheWebAsync,下载网站内容作为字符串并返回该字符串的长度。 AccessTheWebAsync 使用异步 HttpClient 方法 GetStringAsync(String) 来下载内容。

编号显示行显示在程序的策略点中,帮助您了解程序运行状况并解释每个标记点的情况。 显示行标记为“一”到“六”。标签表示程序到达这些代码行的顺序。

下面的代码显示程序的大纲。

Class MainWindow

    Private Async Sub StartButton_Click(sender As Object, e As RoutedEventArgs) Handles StartButton.Click

        ' ONE
        Dim getLengthTask As Task(Of Integer) = AccessTheWebAsync()

        ' FOUR
        Dim contentLength As Integer = Await getLengthTask

        ' SIX
        ResultsTextBox.Text &=
            String.Format(vbCrLf & "Length of the downloaded string: {0}." & vbCrLf, contentLength)

    End Sub


    Async Function AccessTheWebAsync() As Task(Of Integer)

        ' TWO
        Dim client As HttpClient = New HttpClient() 
        Dim getStringTask As Task(Of String) = 
            client.GetStringAsync("https://msdn.microsoft.com")

        ' THREE
        Dim urlContents As String = Await getStringTask

        ' FIVE
        Return urlContents.Length
    End Function

End Class
public partial class MainWindow : Window
{
    // . . .
    private async void startButton_Click(object sender, RoutedEventArgs e)
    {
        // ONE
        Task<int> getLengthTask = AccessTheWebAsync();

        // FOUR
        int contentLength = await getLengthTask;

        // SIX
        resultsTextBox.Text +=
            String.Format("\r\nLength of the downloaded string: {0}.\r\n", contentLength);
    }


    async Task<int> AccessTheWebAsync()
    {
        // TWO
        HttpClient client = new HttpClient();
        Task<string> getStringTask =
            client.GetStringAsync("https://msdn.microsoft.com");

        // THREE                 
        string urlContents = await getStringTask;

        // FIVE
        return urlContents.Length;
    }
}

每个标记的位置(从“1”到“6”)显示有关程序的当前状态的信息。 将生成下面的输出。

ONE:   Entering startButton_Click.
           Calling AccessTheWebAsync.

TWO:   Entering AccessTheWebAsync.
           Calling HttpClient.GetStringAsync.

THREE: Back in AccessTheWebAsync.
           Task getStringTask is started.
           About to await getStringTask & return a Task<int> to startButton_Click.

FOUR:  Back in startButton_Click.
           Task getLengthTask is started.
           About to await getLengthTask -- no caller to return to.

FIVE:  Back in AccessTheWebAsync.
           Task getStringTask is complete.
           Processing the return statement.
           Exiting from AccessTheWebAsync.

SIX:   Back in startButton_Click.
           Task getLengthTask is finished.
           Result from AccessTheWebAsync is stored in contentLength.
           About to display contentLength and exit.

Length of the downloaded string: 33946.

设置程序

您可以从 MSDN 下载本主题使用的代码,也可以自行生成。

备注

要运行此示例,必须在你的计算机上安装 Visual Studio 2012、Visual Studio 2013、Visual Studio Express 2012、Visual Studio Express 2013 for Windows 或 .NET Framework 4.5 或 4.5.1。

下载程序

您可以从 Async 示例:异步程序中的控制流中下载本主题的应用程序。 下列步骤打开并运行该程序。

  1. 解压缩下载的文件,然后启动 Visual Studio。

  2. 在菜单栏上,依次选择**“文件”“打开”“项目/解决方案”**。

  3. 导航到保存存档示例代码的文件夹,然后打开解决方案 (.sln) 文件,然后选择 F5 键来构建和运行项目。

您自行生成程序

下面 Windows Presentation Foundation (WPF) 项目包含此主题的代码示例。

若要运行此程序,请执行以下步骤:

  1. 启动 Visual Studio。

  2. 在菜单栏上,选择**“文件”“新建**、“项目”

    将打开**“新建项目”**对话框。

  3. 在“已安装的模板”窗格中,选择“Visual Basic”或“Visual C#”,然后在项目类型列表中选择“WPF 应用程序”。

  4. 输入 AsyncTracer 作为项目名称,然后选择“确定”按钮。

    新项目出现在**“解决方案资源管理器”**中。

  5. 在 Visual Studio 代码编辑器中,选择“MainWindow.xaml”选项卡。

    如果此选项卡不可视,则在“解决方案资源管理器”中,打开 MainWindow.xaml 的快捷菜单,然后选择“查看代码”。

  6. 在 MainWindow.xaml 的“XAML”视图中,使用下面的代码替换代码。

    <Window
        xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="https://schemas.microsoft.com/expression/blend/2008" xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="MainWindow"
        Title="Control Flow Trace" Height="350" Width="525">
        <Grid>
            <Button x:Name="StartButton" Content="Start" HorizontalAlignment="Left" Margin="221,10,0,0" VerticalAlignment="Top" Width="75"/>
            <TextBox x:Name="ResultsTextBox" HorizontalAlignment="Left" TextWrapping="Wrap" VerticalAlignment="Bottom" Width="510" Height="265" FontFamily="Lucida Console" FontSize="10" VerticalScrollBarVisibility="Visible" d:LayoutOverrides="HorizontalMargin"/>
    
        </Grid>
    </Window>
    
    <Window
            xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="https://schemas.microsoft.com/expression/blend/2008" xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="AsyncTracer.MainWindow"
            Title="Control Flow Trace" Height="350" Width="592">
        <Grid>
            <Button x:Name="startButton" Content="Start&#xa;" HorizontalAlignment="Left" Margin="250,10,0,0" VerticalAlignment="Top" Width="75" Height="24"  Click="startButton_Click" d:LayoutOverrides="GridBox"/>
            <TextBox x:Name="resultsTextBox" HorizontalAlignment="Left" TextWrapping="Wrap" VerticalAlignment="Bottom" Width="576" Height="265" FontFamily="Lucida Console" FontSize="10" VerticalScrollBarVisibility="Visible" Grid.ColumnSpan="3"/>
        </Grid>
    </Window>
    

    包含文本框和按钮的简单窗口显示在 MainWindow.xaml 的“设计”窗口中。

  7. 添加对 System.Net.Http 的引用。

  8. 在“解决方案资源管理器”中,打开“MainWindow.xaml.vb”或“MainWindow.xaml.cs”的快捷菜单,然后选择“查看代码”。

  9. 将 MainWindow.xaml.vb 或 MainWindow.xaml.cs 中的代码替换为以下代码。

    ' Add an Imports statement and a reference for System.Net.Http. 
    Imports System.Net.Http
    
    Class MainWindow
    
        Private Async Sub StartButton_Click(sender As Object, e As RoutedEventArgs) Handles StartButton.Click
    
            ' The display lines in the example lead you through the control shifts.
            ResultsTextBox.Text &= "ONE:   Entering StartButton_Click." & vbCrLf &
                "           Calling AccessTheWebAsync." & vbCrLf
    
            Dim getLengthTask As Task(Of Integer) = AccessTheWebAsync()
    
            ResultsTextBox.Text &= vbCrLf & "FOUR:  Back in StartButton_Click." & vbCrLf &
                "           Task getLengthTask is started." & vbCrLf &
                "           About to await getLengthTask -- no caller to return to." & vbCrLf
    
            Dim contentLength As Integer = Await getLengthTask
    
            ResultsTextBox.Text &= vbCrLf & "SIX:   Back in StartButton_Click." & vbCrLf &
                "           Task getLengthTask is finished." & vbCrLf &
                "           Result from AccessTheWebAsync is stored in contentLength." & vbCrLf &
                "           About to display contentLength and exit." & vbCrLf
    
            ResultsTextBox.Text &=
                String.Format(vbCrLf & "Length of the downloaded string: {0}." & vbCrLf, contentLength)
        End Sub
    
    
        Async Function AccessTheWebAsync() As Task(Of Integer)
    
            ResultsTextBox.Text &= vbCrLf & "TWO:   Entering AccessTheWebAsync." 
    
            ' Declare an HttpClient object. 
            Dim client As HttpClient = New HttpClient()
    
            ResultsTextBox.Text &= vbCrLf & "           Calling HttpClient.GetStringAsync." & vbCrLf
    
            ' GetStringAsync returns a Task(Of String).  
            Dim getStringTask As Task(Of String) = client.GetStringAsync("https://msdn.microsoft.com")
    
            ResultsTextBox.Text &= vbCrLf & "THREE: Back in AccessTheWebAsync." & vbCrLf &
                "           Task getStringTask is started." 
    
            ' AccessTheWebAsync can continue to work until getStringTask is awaited.
    
            ResultsTextBox.Text &=
                vbCrLf & "           About to await getStringTask & return a Task(Of Integer) to StartButton_Click." & vbCrLf
    
            ' Retrieve the website contents when task is complete. 
            Dim urlContents As String = Await getStringTask
    
            ResultsTextBox.Text &= vbCrLf & "FIVE:  Back in AccessTheWebAsync." &
                vbCrLf & "           Task getStringTask is complete." &
                vbCrLf & "           Processing the return statement." &
                vbCrLf & "           Exiting from AccessTheWebAsync." & vbCrLf
    
            Return urlContents.Length
        End Function 
    
    End Class
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
    
    // Add a using directive and a reference for System.Net.Http; 
    using System.Net.Http;
    
    namespace AsyncTracer
    {
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
            }
    
            private async void startButton_Click(object sender, RoutedEventArgs e)
            {
                // The display lines in the example lead you through the control shifts.
                resultsTextBox.Text += "ONE:   Entering startButton_Click.\r\n" +
                    "           Calling AccessTheWebAsync.\r\n";
    
                Task<int> getLengthTask = AccessTheWebAsync();
    
                resultsTextBox.Text += "\r\nFOUR:  Back in startButton_Click.\r\n" +
                    "           Task getLengthTask is started.\r\n" +
                    "           About to await getLengthTask -- no caller to return to.\r\n";
    
                int contentLength = await getLengthTask;
    
                resultsTextBox.Text += "\r\nSIX:   Back in startButton_Click.\r\n" +
                    "           Task getLengthTask is finished.\r\n" +
                    "           Result from AccessTheWebAsync is stored in contentLength.\r\n" +
                    "           About to display contentLength and exit.\r\n";
    
                resultsTextBox.Text +=
                    String.Format("\r\nLength of the downloaded string: {0}.\r\n", contentLength);
            }
    
    
            async Task<int> AccessTheWebAsync()
            {
                resultsTextBox.Text += "\r\nTWO:   Entering AccessTheWebAsync.";
    
                // Declare an HttpClient object.
                HttpClient client = new HttpClient();
    
                resultsTextBox.Text += "\r\n           Calling HttpClient.GetStringAsync.\r\n";
    
                // GetStringAsync returns a Task<string>. 
                Task<string> getStringTask = client.GetStringAsync("https://msdn.microsoft.com");
    
                resultsTextBox.Text += "\r\nTHREE: Back in AccessTheWebAsync.\r\n" +
                    "           Task getStringTask is started.";
    
                // AccessTheWebAsync can continue to work until getStringTask is awaited.
    
                resultsTextBox.Text +=
                    "\r\n           About to await getStringTask and return a Task<int> to startButton_Click.\r\n";
    
                // Retrieve the website contents when task is complete. 
                string urlContents = await getStringTask;
    
                resultsTextBox.Text += "\r\nFIVE:  Back in AccessTheWebAsync." +
                    "\r\n           Task getStringTask is complete." +
                    "\r\n           Processing the return statement." +
                    "\r\n           Exiting from AccessTheWebAsync.\r\n";
    
                return urlContents.Length;
            }
        }
    }
    
  10. 选择 F5 键运行程序,然后选择“开始”按钮。

    应显示下面的输出:

    ONE:   Entering startButton_Click.
               Calling AccessTheWebAsync.
    
    TWO:   Entering AccessTheWebAsync.
               Calling HttpClient.GetStringAsync.
    
    THREE: Back in AccessTheWebAsync.
               Task getStringTask is started.
               About to await getStringTask & return a Task<int> to startButton_Click.
    
    FOUR:  Back in startButton_Click.
               Task getLengthTask is started.
               About to await getLengthTask -- no caller to return to.
    
    FIVE:  Back in AccessTheWebAsync.
               Task getStringTask is complete.
               Processing the return statement.
               Exiting from AccessTheWebAsync.
    
    SIX:   Back in startButton_Click.
               Task getLengthTask is finished.
               Result from AccessTheWebAsync is stored in contentLength.
               About to display contentLength and exit.
    
    Length of the downloaded string: 33946.
    

跟踪程序

步骤一和二

当 startButton_Click 调用 AccessTheWebAsync,且 AccessTheWebAsync 调用异步 HttpClient 方法 GetStringAsync(String) 时,前两个显示行跟踪该路径。 下图概述了从方法到方法的调用。

步骤 1 和步骤 2

AccessTheWebAsync 和 client.GetStringAsync 的返回类型是 Task。 对于 AccessTheWebAsync,TResult 是整数。 对于 GetStringAsync,TResult 是字符串。 有关同步方法返回类型的详细信息,请参见 异步返回类型(C# 和 Visual Basic)

当控件转换回调用方时,任务返回的异步方法返回任务实例。 当调用方法中出现 Await 或 await 运算符,或当调用方法结束时,控件从异步方法返回其调用方。 标记为“三”到“六”的显示行跟踪该进程的此部分。

步骤三

在 AccessTheWebAsync 中,调用异步方法 GetStringAsync(String) 可下载目标网页的内容。 控件从 client.GetStringAsync 返回到 AccessTheWebAsync,当 client.GetStringAsync 返回。

client.GetStringAsync 方法返回分配给 AccessTheWebAsync 中 getStringTask 变量的字符串的任务。 示例程序中的以下行显示对 client.GetStringAsync 的调用和分配。

Dim getStringTask As Task(Of String) = client.GetStringAsync("https://msdn.microsoft.com")
Task<string> getStringTask = client.GetStringAsync("https://msdn.microsoft.com");

您可以将任务视为 client.GetStringAsync 做出的承诺,以便最终生成一个实际字符串。 同时,如果 AccessTheWebAsync 要执行不依赖 client.GetStringAsync 承诺的字符串的工作,则该工作在 client.GetStringAsync 等待时继续。 在示例中,以下输出行(标记为“THREE”)表示可以单独操作。

THREE: Back in AccessTheWebAsync.
           Task getStringTask is started.
           About to await getStringTask & return a Task<int> to startButton_Click.

当等待 getStringTask 时,下面的语句会挂起 AccessTheWebAsync 中的进程。

Dim urlContents As String = Await getStringTask
string urlContents = await getStringTask;

下图显示了从 client.GetStringAsync 到 getStringTask 的分配和从 getStringTask 的创建到等待运算符的应用程序的控制流。

步骤 3

等待表达式挂起 AccessTheWebAsync 直到 client.GetStringAsync 返回。 同时,控件返回至 AccessTheWebAsync、startButton_Click 的调用方。

备注

通常情况下,你会立即等待调用异步方法。例如,以下分配之一可替换先前创建的代码,然后等待 getStringTask:

  • Visual Basic:Dim urlContents As String = Await client.GetStringAsync("https://msdn.microsoft.com")

  • C#:string urlContents = await client.GetStringAsync("https://msdn.microsoft.com");

在本主题中,稍后将运用等待运算符通过程序调节标记控制流的输出行。

步骤四

AccessTheWebAsync 的声明的返回类型是 Visual Basic 中的 Task(Of Integer) 和 C# 中的 Task<int>。 因此,当 AccessTheWebAsync 挂起时,它向 startButton_Click 返回整数任务。 你应了解返回的任务并不是 getStringTask。 返回的任务是表示挂起的方法中仍要执行的整数的新任务 AccessTheWebAsync。 当任务完成时,任务是 AccessTheWebAsync 中的一个承诺以生成整数。

下面的语句将此任务分配给 getLengthTask 变量。

Dim getLengthTask As Task(Of Integer) = AccessTheWebAsync()
Task<int> getLengthTask = AccessTheWebAsync();

正如在 AccessTheWebAsync 中,startButton_Click 可继续那些不依赖于异步任务结果的工作 (getLengthTask) 直到任务处于等待状态。 下面的输出行表示该工作。

FOUR:  Back in startButton_Click.
           Task getLengthTask is started.
           About to await getLengthTask -- no caller to return to.

当 getLengthTask 等待时,startButton_Click 中的进程将被挂起。 下面的赋值语句会挂起 startButton_Click 直到 AccessTheWebAsync 完成。

Dim contentLength As Integer = Await getLengthTask
int contentLength = await getLengthTask;

在下图中,箭头显示从 AccessTheWebAsync 中的 await 表达式到赋值给 getLengthTask 的控制流,接着是在 startButton_Click 中进行一般处理,直到等待 getLengthTask。

步骤 4

步骤五

如果 client.GetStringAsync 发出了已完成信号,AccessTheWebAsync 中的处理过程就可以从挂起中释放并继续到 await 语句后。 输出的以下行表示处理的恢复。

FIVE:  Back in AccessTheWebAsync.
           Task getStringTask is complete.
           Processing the return statement.
           Exiting from AccessTheWebAsync.

返回语句 urlContents.Length 的操作存储在 AccessTheWebAsync 返回的任务中。 等待表达式从 startButton_Click 中的 getLengthTask 检索该值。

下图显示 client.GetStringAsync(和 getStringTask)完成后的控件传输。

步骤 5

AccessTheWebAsync 已完成运行,并且控件返回到 startButton_Click,正等待完成。

步骤六

如果 AccessTheWebAsync 发出了已完成信号,处理过程就可以继续到 startButton_Async中的 await 语句后。 实际上,程序没有其他要执行的操作。

输出的以下行表示 startButton_Async 中处理的恢复:

SIX:   Back in startButton_Click.
           Task getLengthTask is finished.
           Result from AccessTheWebAsync is stored in contentLength.
           About to display contentLength and exit.

等待表达式是从作为 AccessTheWebAsync 中返回语句的操作数的 getLengthTask 整数值中检索的。 下面的语句将该值分配给 contentLength 变量。

Dim contentLength As Integer = Await getLengthTask
int contentLength = await getLengthTask;

下图显示了从 AccessTheWebAsync 到 startButton_Click 的控制返回。

步骤 6

请参见

任务

演练:使用 Async 和 Await 访问 Web(C# 和 Visual Basic)

演练:将调试器与异步方法一起使用

概念

使用 Async 和 Await 的异步编程(C# 和 Visual Basic)

异步返回类型(C# 和 Visual Basic)

其他资源

Async Sample: Control Flow in Async Programs (C# and Visual Basic)