处理异步应用中的重新进入 (C#)Handling Reentrancy in Async Apps (C#)

在应用中包含异步代码时,应考虑并且可以阻止重新进入(指在异步操作完成之前重新进入它)。When you include asynchronous code in your app, you should consider and possibly prevent reentrancy, which refers to reentering an asynchronous operation before it has completed. 如果不识别并处理重新进入的可能性,则它可能会导致意外结果。If you don't identify and handle possibilities for reentrancy, it can cause unexpected results.

在本主题中In this topic

备注

若要运行该示例,计算机上必须安装 Visual Studio 2012 或更高版本和 .NET Framework 4.5 或更高版本。To run the example, you must have Visual Studio 2012 or newer and the .NET Framework 4.5 or newer installed on your computer.

识别重新进入Recognizing Reentrancy

在本主题中的示例中,用户选择“开始” 按钮以启动一个异步应用,该应用下载一系列网站并计算下载的总字节数。In the example in this topic, users choose a Start button to initiate an asynchronous app that downloads a series of websites and calculates the total number of bytes that are downloaded. 该示例的同步版本以相同方式进行响应(无论用户选择该按钮多少次),因为在第一次选择之后,UI 线程会忽略这些事件,直到应用完成运行。A synchronous version of the example would respond the same way regardless of how many times a user chooses the button because, after the first time, the UI thread ignores those events until the app finishes running. 但是,在异步应用中,UI 线程会继续响应,你可能会在它完成之前重新进入异步操作。In an asynchronous app, however, the UI thread continues to respond, and you might reenter the asynchronous operation before it has completed.

下面的示例显示用户仅选择“开始” 按钮一次时的预期输出。The following example shows the expected output if the user chooses the Start button only once. 下载网站的列表会出现,其中包含每个站点的大小(以字节为单位)。A list of the downloaded websites appears with the size, in bytes, of each site. 总字节数会在结尾处显示。The total number of bytes appears at the end.

1. msdn.microsoft.com/library/hh191443.aspx                83732
2. msdn.microsoft.com/library/aa578028.aspx               205273
3. msdn.microsoft.com/library/jj155761.aspx                29019
4. msdn.microsoft.com/library/hh290140.aspx               117152
5. msdn.microsoft.com/library/hh524395.aspx                68959
6. msdn.microsoft.com/library/ms404677.aspx               197325
7. msdn.microsoft.com                                            42972
8. msdn.microsoft.com/library/ff730837.aspx               146159

TOTAL bytes returned:  890591

但是,如果用户多次选择按钮,则会反复调用事件处理程序,并且每次都会重新进入下载进程。However, if the user chooses the button more than once, the event handler is invoked repeatedly, and the download process is reentered each time. 因此,会有多个异步操作同时运行,输出会使结果交错,而总字节数令人困惑。As a result, several asynchronous operations are running at the same time, the output interleaves the results, and the total number of bytes is confusing.

1. msdn.microsoft.com/library/hh191443.aspx                83732
2. msdn.microsoft.com/library/aa578028.aspx               205273
3. msdn.microsoft.com/library/jj155761.aspx                29019
4. msdn.microsoft.com/library/hh290140.aspx               117152
5. msdn.microsoft.com/library/hh524395.aspx                68959
1. msdn.microsoft.com/library/hh191443.aspx                83732
2. msdn.microsoft.com/library/aa578028.aspx               205273
6. msdn.microsoft.com/library/ms404677.aspx               197325
3. msdn.microsoft.com/library/jj155761.aspx                29019
7. msdn.microsoft.com                                            42972
4. msdn.microsoft.com/library/hh290140.aspx               117152
8. msdn.microsoft.com/library/ff730837.aspx               146159

TOTAL bytes returned:  890591

5. msdn.microsoft.com/library/hh524395.aspx                68959
1. msdn.microsoft.com/library/hh191443.aspx                83732
2. msdn.microsoft.com/library/aa578028.aspx               205273
6. msdn.microsoft.com/library/ms404677.aspx               197325
3. msdn.microsoft.com/library/jj155761.aspx                29019
4. msdn.microsoft.com/library/hh290140.aspx               117152
7. msdn.microsoft.com                                            42972
5. msdn.microsoft.com/library/hh524395.aspx                68959
8. msdn.microsoft.com/library/ff730837.aspx               146159

TOTAL bytes returned:  890591

6. msdn.microsoft.com/library/ms404677.aspx               197325
7. msdn.microsoft.com                                            42972
8. msdn.microsoft.com/library/ff730837.aspx               146159

TOTAL bytes returned:  890591

可以滚动到本主题末尾来评审生成此输出的代码。You can review the code that produces this output by scrolling to the end of this topic. 可以通过将解决方案下载到本地计算机,然后运行 WebsiteDownload 项目,或是通过使用本主题末尾的代码创建自己的项目,来体验该代码。You can experiment with the code by downloading the solution to your local computer and then running the WebsiteDownload project or by using the code at the end of this topic to create your own project. 有关详细信息和说明,请参阅检查并运行示例应用For more information and instructions, see Reviewing and Running the Example App.

处理重新进入Handling Reentrancy

可以采用各种方式处理重新进入,具体取决于希望应用执行的操作。You can handle reentrancy in a variety of ways, depending on what you want your app to do. 本主题展示了以下示例:This topic presents the following examples:

禁用“开始”按钮Disable the Start Button

可以通过在 StartButton_Click 事件处理程序顶部禁用“开始” 按钮,在操作运行期间阻止该按钮。You can block the Start button while an operation is running by disabling the button at the top of the StartButton_Click event handler. 随后可以在操作完成时从 finally 块中重新启用中该按钮,以便用户可以再次运行应用。You can then reenable the button from within a finally block when the operation finishes so that users can run the app again.

若要设置此方案,请对检查并运行示例应用中提供的基本代码进行以下更改。To set up this scenario, make the following changes to the basic code that is provided in Reviewing and Running the Example App. 还可以从异步示例:.NET 桌面应用中的重新进入下载已完成的应用。You also can download the finished app from Async Samples: Reentrancy in .NET Desktop Apps. 项目名是 DisableStartButton。The name of the project is DisableStartButton.

private async void StartButton_Click(object sender, RoutedEventArgs e)
{
    // This line is commented out to make the results clearer in the output.
    //ResultsTextBox.Text = "";

    // ***Disable the Start button until the downloads are complete.
    StartButton.IsEnabled = false;

    try
    {
        await AccessTheWebAsync();
    }
    catch (Exception)
    {
        ResultsTextBox.Text += "\r\nDownloads failed.";
    }
    // ***Enable the Start button in case you want to run the program again.
    finally
    {
        StartButton.IsEnabled = true;
    }
}

由于进行了这些更改,所以该按钮在 AccessTheWebAsync 下载网站期间不会进行响应,因此无法重新进入该进程。As a result of the changes, the button doesn't respond while AccessTheWebAsync is downloading the websites, so the process can’t be reentered.

取消和重启操作Cancel and Restart the Operation

可以使“开始” 按钮保持活动状态而不是禁用该按钮,但是如果用户再次选择该按钮,则取消已在运行的操作,让最近开始的操作继续运行。Instead of disabling the Start button, you can keep the button active but, if the user chooses that button again, cancel the operation that's already running and let the most recently started operation continue.

有关取消的详细信息,请参阅微调异步应用程序 (C#)For more information about cancellation, see Fine-Tuning Your Async Application (C#).

若要设置此方案,请对检查并运行示例应用中提供的基本代码进行以下更改。To set up this scenario, make the following changes to the basic code that is provided in Reviewing and Running the Example App. 还可以从异步示例:.NET 桌面应用中的重新进入下载已完成的应用。You also can download the finished app from Async Samples: Reentrancy in .NET Desktop Apps. 项目名是 CancelAndRestart。The name of the project is CancelAndRestart.

  1. 声明 CancellationTokenSource 变量 cts,它处于所有方法的范围内。Declare a CancellationTokenSource variable, cts, that’s in scope for all methods.

    public partial class MainWindow : Window   // Or class MainPage
    {
        // *** Declare a System.Threading.CancellationTokenSource.
        CancellationTokenSource cts;
    
  2. StartButton_Click 中,确定操作是否已在进行。In StartButton_Click, determine whether an operation is already underway. 如果 cts 的值为 null,则没有任何操作处于活动状态。If the value of cts is null, no operation is already active. 如果该值不为 null,则取消已在运行的操作。If the value isn't null, the operation that is already running is canceled.

    // *** If a download process is already underway, cancel it.
    if (cts != null)
    {
        cts.Cancel();
    }
    
  3. cts 设置为表示当前进程的不同值。Set cts to a different value that represents the current process.

    // *** Now set cts to a new value that you can use to cancel the current process
    // if the button is chosen again.
    CancellationTokenSource newCTS = new CancellationTokenSource();
    cts = newCTS;
    
  4. StartButton_Click 末尾,当前进程完成,因此将 cts 的值重新设置为 null。At the end of StartButton_Click, the current process is complete, so set the value of cts back to null.

    // *** When the process is complete, signal that another process can begin.
    if (cts == newCTS)
        cts = null;
    

以下代码显示了 StartButton_Click 中的所有更改。The following code shows all the changes in StartButton_Click. 新增内容标有星号。The additions are marked with asterisks.

private async void StartButton_Click(object sender, RoutedEventArgs e)
{
    // This line is commented out to make the results clearer in the output.
    //ResultsTextBox.Clear();

    // *** If a download process is already underway, cancel it.
    if (cts != null)
    {
        cts.Cancel();
    }

    // *** Now set cts to cancel the current process if the button is chosen again.
    CancellationTokenSource newCTS = new CancellationTokenSource();
    cts = newCTS;

    try
    {
        // ***Send cts.Token to carry the message if there is a cancellation request.
        await AccessTheWebAsync(cts.Token);

    }
    // *** Catch cancellations separately.
    catch (OperationCanceledException)
    {
        ResultsTextBox.Text += "\r\nDownloads canceled.\r\n";
    }
    catch (Exception)
    {
        ResultsTextBox.Text += "\r\nDownloads failed.\r\n";
    }
    // *** When the process is complete, signal that another process can proceed.
    if (cts == newCTS)
        cts = null;
}

AccessTheWebAsync 中,进行下列更改。In AccessTheWebAsync, make the following changes.

  • 添加参数以从 StartButton_Click 接受取消标记。Add a parameter to accept the cancellation token from StartButton_Click.

  • 使用 GetAsync 方法下载网站,因为 GetAsync 接受 CancellationToken 参数。Use the GetAsync method to download the websites because GetAsync accepts a CancellationToken argument.

  • 调用 DisplayResults 以显示下载的每个网站的结果之前,检查 ct 以验证当前操作是否未取消。Before calling DisplayResults to display the results for each downloaded website, check ct to verify that the current operation hasn’t been canceled.

下面的代码演示了这些更改(使用星号标记)。The following code shows these changes, which are marked with asterisks.

// *** Provide a parameter for the CancellationToken from StartButton_Click.
async Task AccessTheWebAsync(CancellationToken ct)
{
    // Declare an HttpClient object.
    HttpClient client = new HttpClient();

    // Make a list of web addresses.
    List<string> urlList = SetUpURLList();

    var total = 0;
    var position = 0;

    foreach (var url in urlList)
    {
        // *** Use the HttpClient.GetAsync method because it accepts a
        // cancellation token.
        HttpResponseMessage response = await client.GetAsync(url, ct);

        // *** Retrieve the website contents from the HttpResponseMessage.
        byte[] urlContents = await response.Content.ReadAsByteArrayAsync();

        // *** Check for cancellations before displaying information about the
        // latest site.
        ct.ThrowIfCancellationRequested();

        DisplayResults(url, urlContents, ++position);

        // Update the total.
        total += urlContents.Length;
    }

    // Display the total count for all of the websites.
    ResultsTextBox.Text +=
        $"\r\n\r\nTOTAL bytes returned:  {total}\r\n";
}

如果在此应用运行期间多次选择“开始” 按钮,则它应生成类似于以下输出的结果。If you choose the Start button several times while this app is running, it should produce results that resemble the following output.

1. msdn.microsoft.com/library/hh191443.aspx                83732
2. msdn.microsoft.com/library/aa578028.aspx               205273
3. msdn.microsoft.com/library/jj155761.aspx                29019
4. msdn.microsoft.com/library/hh290140.aspx               122505
5. msdn.microsoft.com/library/hh524395.aspx                68959
6. msdn.microsoft.com/library/ms404677.aspx               197325
Download canceled.

1. msdn.microsoft.com/library/hh191443.aspx                83732
2. msdn.microsoft.com/library/aa578028.aspx               205273
3. msdn.microsoft.com/library/jj155761.aspx                29019
Download canceled.

1. msdn.microsoft.com/library/hh191443.aspx                83732
2. msdn.microsoft.com/library/aa578028.aspx               205273
3. msdn.microsoft.com/library/jj155761.aspx                29019
4. msdn.microsoft.com/library/hh290140.aspx               117152
5. msdn.microsoft.com/library/hh524395.aspx                68959
6. msdn.microsoft.com/library/ms404677.aspx               197325
7. msdn.microsoft.com                                            42972
8. msdn.microsoft.com/library/ff730837.aspx               146159

TOTAL bytes returned:  890591

若要消除部分列表,请对 StartButton_Click 中的第一行代码取消注释以在用户每次重新启动操作时清除文本框。To eliminate the partial lists, uncomment the first line of code in StartButton_Click to clear the text box each time the user restarts the operation.

运行多个操作并将输出排入队列Run Multiple Operations and Queue the Output

此第三个示例最复杂,因为应用会在用户每次选择“开始” 按钮时启动另一个异步操作,并且所有操作都会运行到完成。This third example is the most complicated in that the app starts another asynchronous operation each time that the user chooses the Start button, and all the operations run to completion. 所有请求的操作以异步方式从列表中下载网站,但是操作的输出会按顺序呈现。All the requested operations download websites from the list asynchronously, but the output from the operations is presented sequentially. 也就是说,实际下载活动是交错进行的(如识别重新进入中的输出所示),但是每个组的结果列表会分开呈现。That is, the actual downloading activity is interleaved, as the output in Recognizing Reentrancy shows, but the list of results for each group is presented separately.

操作会共享一个全局 Task (pendingWork),它用作显示进程的守卫。The operations share a global Task, pendingWork, which serves as a gatekeeper for the display process.

若要设置此方案,请对检查并运行示例应用中提供的基本代码进行以下更改。To set up this scenario, make the following changes to the basic code that is provided in Reviewing and Running the Example App. 还可以从异步示例:.NET 桌面应用中的重新进入下载已完成的应用。You also can download the finished app from Async Samples: Reentrancy in .NET Desktop Apps. 项目名是 QueueResults。The name of the project is QueueResults.

下面的输出显示用户仅选择“开始” 按钮一次时的结果。The following output shows the result if the user chooses the Start button only once. 字母标签 A 指示结果来自首次选择“开始” 按钮。The letter label, A, indicates that the result is from the first time the Start button is chosen. 编号显示下载目标列表中 URL 的顺序。The numbers show the order of the URLs in the list of download targets.

#Starting group A.
#Task assigned for group A.

A-1. msdn.microsoft.com/library/hh191443.aspx                87389
A-2. msdn.microsoft.com/library/aa578028.aspx               209858
A-3. msdn.microsoft.com/library/jj155761.aspx                30870
A-4. msdn.microsoft.com/library/hh290140.aspx               119027
A-5. msdn.microsoft.com/library/hh524395.aspx                71260
A-6. msdn.microsoft.com/library/ms404677.aspx               199186
A-7. msdn.microsoft.com                                            53266
A-8. msdn.microsoft.com/library/ff730837.aspx               148020

TOTAL bytes returned:  918876

#Group A is complete.

如果用户选择“开始” 按钮三次,则应用会生成类似于以下各行的输出。If the user chooses the Start button three times, the app produces output that resembles the following lines. 以井号 (#) 开头的信息行会跟踪应用程序的进度。The information lines that start with a pound sign (#) trace the progress of the application.

#Starting group A.
#Task assigned for group A.

A-1. msdn.microsoft.com/library/hh191443.aspx                87389
A-2. msdn.microsoft.com/library/aa578028.aspx               207089
A-3. msdn.microsoft.com/library/jj155761.aspx                30870
A-4. msdn.microsoft.com/library/hh290140.aspx               119027
A-5. msdn.microsoft.com/library/hh524395.aspx                71259
A-6. msdn.microsoft.com/library/ms404677.aspx               199185

#Starting group B.
#Task assigned for group B.

A-7. msdn.microsoft.com                                            53266

#Starting group C.
#Task assigned for group C.

A-8. msdn.microsoft.com/library/ff730837.aspx               148010

TOTAL bytes returned:  916095

B-1. msdn.microsoft.com/library/hh191443.aspx                87389
B-2. msdn.microsoft.com/library/aa578028.aspx               207089
B-3. msdn.microsoft.com/library/jj155761.aspx                30870
B-4. msdn.microsoft.com/library/hh290140.aspx               119027
B-5. msdn.microsoft.com/library/hh524395.aspx                71260
B-6. msdn.microsoft.com/library/ms404677.aspx               199186

#Group A is complete.

B-7. msdn.microsoft.com                                            53266
B-8. msdn.microsoft.com/library/ff730837.aspx               148010

TOTAL bytes returned:  916097

C-1. msdn.microsoft.com/library/hh191443.aspx                87389
C-2. msdn.microsoft.com/library/aa578028.aspx               207089

#Group B is complete.

C-3. msdn.microsoft.com/library/jj155761.aspx                30870
C-4. msdn.microsoft.com/library/hh290140.aspx               119027
C-5. msdn.microsoft.com/library/hh524395.aspx                72765
C-6. msdn.microsoft.com/library/ms404677.aspx               199186
C-7. msdn.microsoft.com                                            56190
C-8. msdn.microsoft.com/library/ff730837.aspx               148010

TOTAL bytes returned:  920526

#Group C is complete.

组 B 和 C 会在组 A 完成之前开始,但是每个组的输出会分开显示。Groups B and C start before group A has finished, but the output for the each group appears separately. 组 A 的所有输出会首先显示,后跟组 B 的所有输出,再然后是组 C 的所有输出。应用会始终按顺序显示组,并且对于每个组,始终按 URL 在 URL 列表中的显示顺序来显示有关各个网站的信息。All the output for group A appears first, followed by all the output for group B, and then all the output for group C. The app always displays the groups in order and, for each group, always displays the information about the individual websites in the order that the URLs appear in the list of URLs.

但是,无法预测下载实际发生的顺序。However, you can't predict the order in which the downloads actually happen. 在多个组启动之后,它们生成的下载任务都处于活动状态。After multiple groups have been started, the download tasks that they generate are all active. 无法假定 A-1 会在 B-1 之前下载,并且无法假定 A-1 会在 A-2 之前下载。You can't assume that A-1 will be downloaded before B-1, and you can't assume that A-1 will be downloaded before A-2.

全局定义Global Definitions

示例代码包含所有方法都可见的以下两个全局声明。The sample code contains the following two global declarations that are visible from all methods.

public partial class MainWindow : Window  // Class MainPage in Windows Store app.
{
    // ***Declare the following variables where all methods can access them.
    private Task pendingWork = null;
    private char group = (char)('A' - 1);

Task 变量 pendingWork 会监视显示进程并防止任何组中断另一个组的显示操作。The Task variable, pendingWork, oversees the display process and prevents any group from interrupting another group's display operation. 字符变量 group 标记来自不同组的输出以验证结果是否按预期顺序出现。The character variable, group, labels the output from different groups to verify that results appear in the expected order.

单击事件处理程序The Click Event Handler

事件处理程序 StartButton_Click 会在用户每次选择“开始” 按钮时增加组号。The event handler, StartButton_Click, increments the group letter each time the user chooses the Start button. 随后处理程序会调用 AccessTheWebAsync 以运行下载操作。Then the handler calls AccessTheWebAsync to run the downloading operation.

private async void StartButton_Click(object sender, RoutedEventArgs e)
{
    // ***Verify that each group's results are displayed together, and that
    // the groups display in order, by marking each group with a letter.
    group = (char)(group + 1);
    ResultsTextBox.Text += $"\r\n\r\n#Starting group {group}.";

    try
    {
        // *** Pass the group value to AccessTheWebAsync.
        char finishedGroup = await AccessTheWebAsync(group);

        // The following line verifies a successful return from the download and
        // display procedures.
        ResultsTextBox.Text += $"\r\n\r\n#Group {finishedGroup} is complete.\r\n";
    }
    catch (Exception)
    {
        ResultsTextBox.Text += "\r\nDownloads failed.";
    }
}

AccessTheWebAsync 方法The AccessTheWebAsync Method

此示例将 AccessTheWebAsync 拆分为两个方法。This example splits AccessTheWebAsync into two methods. 第一个方法 AccessTheWebAsync 会为组启动所有下载任务并设置 pendingWork 以控制显示进程。The first method, AccessTheWebAsync, starts all the download tasks for a group and sets up pendingWork to control the display process. 该方法使用语言集成查询(LINQ 查询)和 ToArray 同时启动所有下载任务。The method uses a Language Integrated Query (LINQ query) and ToArray to start all the download tasks at the same time.

AccessTheWebAsync 随后调用 FinishOneGroupAsync 以等待每个下载完成并显示其长度。AccessTheWebAsync then calls FinishOneGroupAsync to await the completion of each download and display its length.

FinishOneGroupAsync 会返回在 AccessTheWebAsync 中分配给 pendingWork 的任务。FinishOneGroupAsync returns a task that's assigned to pendingWork in AccessTheWebAsync. 该值会在任务完成之前阻止另一个操作进行中断。That value prevents interruption by another operation before the task is complete.

private async Task<char> AccessTheWebAsync(char grp)
{
    HttpClient client = new HttpClient();

    // Make a list of the web addresses to download.
    List<string> urlList = SetUpURLList();

    // ***Kick off the downloads. The application of ToArray activates all the download tasks.
    Task<byte[]>[] getContentTasks = urlList.Select(url => client.GetByteArrayAsync(url)).ToArray();

    // ***Call the method that awaits the downloads and displays the results.
    // Assign the Task that FinishOneGroupAsync returns to the gatekeeper task, pendingWork.
    pendingWork = FinishOneGroupAsync(urlList, getContentTasks, grp);

    ResultsTextBox.Text += $"\r\n#Task assigned for group {grp}. Download tasks are active.\r\n";

    // ***This task is complete when a group has finished downloading and displaying.
    await pendingWork;

    // You can do other work here or just return.
    return grp;
}

FinishOneGroupAsync 方法The FinishOneGroupAsync Method

此方法会循环访问组中的下载任务(等待每个任务、显示下载网站的长度并将该长度添加到总和中)。This method cycles through the download tasks in a group, awaiting each one, displaying the length of the downloaded website, and adding the length to the total.

FinishOneGroupAsync 中的第一个语句使用 pendingWork 确保进入方法不会干扰已在显示进程中或已在等待的操作。The first statement in FinishOneGroupAsync uses pendingWork to make sure that entering the method doesn't interfere with an operation that is already in the display process or that's already waiting. 如果这类操作正在进行,则进入操作必须等待轮到它。If such an operation is in progress, the entering operation must wait its turn.

private async Task FinishOneGroupAsync(List<string> urls, Task<byte[]>[] contentTasks, char grp)
{
    // ***Wait for the previous group to finish displaying results.
    if (pendingWork != null) await pendingWork;

    int total = 0;

    // contentTasks is the array of Tasks that was created in AccessTheWebAsync.
    for (int i = 0; i < contentTasks.Length; i++)
    {
        // Await the download of a particular URL, and then display the URL and
        // its length.
        byte[] content = await contentTasks[i];
        DisplayResults(urls[i], content, i, grp);
        total += content.Length;
    }

    // Display the total count for all of the websites.
    ResultsTextBox.Text +=
        $"\r\n\r\nTOTAL bytes returned:  {total}\r\n";
}

兴趣点Points of Interest

输出中以井号 (#) 开头的信息行阐明了此示例的工作原理。The information lines that start with a pound sign (#) in the output clarify how this example works.

输出演示以下模式。The output shows the following patterns.

  • 一个组可以上一组显示其输出期间启动,但上一组的输出显示不会中断。A group can be started while a previous group is displaying its output, but the display of the previous group's output isn't interrupted.

    #Starting group A.
    #Task assigned for group A. Download tasks are active.
    
    A-1. msdn.microsoft.com/library/hh191443.aspx                87389
    A-2. msdn.microsoft.com/library/aa578028.aspx               207089
    A-3. msdn.microsoft.com/library/jj155761.aspx                30870
    A-4. msdn.microsoft.com/library/hh290140.aspx               119037
    A-5. msdn.microsoft.com/library/hh524395.aspx                71260
    
    #Starting group B.
    #Task assigned for group B. Download tasks are active.
    
    A-6. msdn.microsoft.com/library/ms404677.aspx               199186
    A-7. msdn.microsoft.com                                            53078
    A-8. msdn.microsoft.com/library/ff730837.aspx               148010
    
    TOTAL bytes returned:  915919
    
    B-1. msdn.microsoft.com/library/hh191443.aspx                87388
    B-2. msdn.microsoft.com/library/aa578028.aspx               207089
    B-3. msdn.microsoft.com/library/jj155761.aspx                30870
    
    #Group A is complete.
    
    B-4. msdn.microsoft.com/library/hh290140.aspx               119027
    B-5. msdn.microsoft.com/library/hh524395.aspx                71260
    B-6. msdn.microsoft.com/library/ms404677.aspx               199186
    B-7. msdn.microsoft.com                                            53078
    B-8. msdn.microsoft.com/library/ff730837.aspx               148010
    
    TOTAL bytes returned:  915908
    
  • 仅对于组 A(它首先启动),pendingWork 任务才在 FinishOneGroupAsync 启动时为 null。The pendingWork task is null at the start of FinishOneGroupAsync only for group A, which started first. 组 A 在它到达 FinishOneGroupAsync 时尚未尚未完成 await 表达式。Group A hasn’t yet completed an await expression when it reaches FinishOneGroupAsync. 因此,控制权未返回给 AccessTheWebAsync,对 pendingWork 的第一个分配尚未发生。Therefore, control hasn't returned to AccessTheWebAsync, and the first assignment to pendingWork hasn't occurred.

  • 下面两行始终在输出中一起显示。The following two lines always appear together in the output. 该代码从不会在于 StartButton_Click 中启动组操作与将组的任务分配给 pendingWork 之间中断。The code is never interrupted between starting a group's operation in StartButton_Click and assigning a task for the group to pendingWork.

    #Starting group B.
    #Task assigned for group B. Download tasks are active.
    

    组进入 StartButton_Click 之后,操作在操作进入 FinishOneGroupAsync 之前不会完成 await 表达式。After a group enters StartButton_Click, the operation doesn't complete an await expression until the operation enters FinishOneGroupAsync. 因此,没有其他操作可以在代码段期间获得控制权。Therefore, no other operation can gain control during that segment of code.

检查并运行示例应用Reviewing and Running the Example App

若要更好地了解示例应用,可以下载它,自己生成或查看本主题末尾的代码,而无需实现应用。To better understand the example app, you can download it, build it yourself, or review the code at the end of this topic without implementing the app.

备注

若要将示例作为 Windows Presentation Foundation (WPF) 桌面应用运行,计算机上必须安装有 Visual Studio 2012 或更高版本和 .NET Framework 4.5 或更高版本。To run the example as a Windows Presentation Foundation (WPF) desktop app, you must have Visual Studio 2012 or newer and the .NET Framework 4.5 or newer installed on your computer.

下载应用Downloading the App

  1. 异步示例:.NET 桌面应用中的重新进入下载压缩文件。Download the compressed file from Async Samples: Reentrancy in .NET Desktop Apps.

  2. 解压缩下载的文件,然后启动 Visual Studio。Decompress the file that you downloaded, and then start Visual Studio.

  3. 在菜单栏上,依次选择 “文件”“打开”“项目/解决方案”On the menu bar, choose File, Open, Project/Solution.

  4. 导航到保存解压缩的示例代码的文件夹,然后打开解决方案 (.sln) 文件。Navigate to the folder that holds the decompressed sample code, and then open the solution (.sln) file.

  5. 在“解决方案资源管理器” 中,打开要运行的项目的快捷菜单,然后选择“设置为 StartUpProject” 。In Solution Explorer, open the shortcut menu for the project that you want to run, and then choose Set as StartUpProject.

  6. 选择 CTRL+F5 键以生成并运行项目。Choose the CTRL+F5 keys to build and run the project.

生成应用Building the App

以下部分提供用于将示例生成为 WPF 应用的代码。The following section provides the code to build the example as a WPF app.

生成 WPF 应用程序To build a WPF app
  1. 启动 Visual Studio。Start Visual Studio.

  2. 在菜单栏上,依次选择“文件” 、“新建” 、“项目” 。On the menu bar, choose File, New, Project.

    “新建项目” 对话框随即打开。The New Project dialog box opens.

  3. 在“已安装的模板” 窗格中,展开“Visual C#” ,然后展开“Windows” 。In the Installed Templates pane, expand Visual C#, and then expand Windows.

  4. 在项目类型列表中,选择“WPF 应用程序” 。In the list of project types, choose WPF Application.

  5. 将项目命名为 WebsiteDownloadWPF,然后选择“确定” 按钮。Name the project WebsiteDownloadWPF, and then choose the OK button.

    新项目将出现在“解决方案资源管理器” 中。The new project appears in Solution Explorer.

  6. 在 Visual Studio 代码编辑器中,选择 “MainWindow.xaml” 选项卡。In the Visual Studio Code Editor, choose the MainWindow.xaml tab.

    如果此选项卡不可见,则在“解决方案资源管理器” 中,打开 MainWindow.xaml 的快捷菜单,然后选择“查看代码” 。If the tab isn’t visible, open the shortcut menu for MainWindow.xaml in Solution Explorer, and then choose View Code.

  7. 在 MainWindow.xaml 的“XAML” 视图中,将代码替换为以下代码。In the XAML view of MainWindow.xaml, replace the code with the following code.

    <Window x:Class="WebsiteDownloadWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="using:WebsiteDownloadWPF"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d">
    
        <Grid Width="517" Height="360">
            <Button x:Name="StartButton" Content="Start" HorizontalAlignment="Left" Margin="-1,0,0,0" VerticalAlignment="Top" Click="StartButton_Click" Height="53" Background="#FFA89B9B" FontSize="36" Width="518"  />
            <TextBox x:Name="ResultsTextBox" HorizontalAlignment="Left" Margin="-1,53,0,-36" TextWrapping="Wrap" VerticalAlignment="Top" Height="343" FontSize="10" ScrollViewer.VerticalScrollBarVisibility="Visible" Width="518" FontFamily="Lucida Console" />
        </Grid>
    </Window>
    

    MainWindow.xaml 的“设计” 视图中将显示一个简单的窗口,其中包含一个文本框和一个按钮。A simple window that contains a text box and a button appears in the Design view of MainWindow.xaml.

  8. System.Net.Http 添加引用。Add a reference for System.Net.Http.

  9. 在“解决方案资源管理器” 中,打开 MainWindow.xaml.cs 的快捷菜单,然后选择“查看代码” 。In Solution Explorer, open the shortcut menu for MainWindow.xaml.cs, and then choose View Code.

  10. 在 MainWindow.xaml.cs 中,将代码替换为以下代码。In MainWindow.xaml.cs, replace the code with the following code.

    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 the following using directives, and add a reference for System.Net.Http.
    using System.Net.Http;
    using System.Threading;
    
    namespace WebsiteDownloadWPF
    {
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
            }
    
            private async void StartButton_Click(object sender, RoutedEventArgs e)
            {
                // This line is commented out to make the results clearer in the output.
                //ResultsTextBox.Text = "";
    
                try
                {
                    await AccessTheWebAsync();
                }
                catch (Exception)
                {
                    ResultsTextBox.Text += "\r\nDownloads failed.";
                }
            }
    
            private async Task AccessTheWebAsync()
            {
                // Declare an HttpClient object.
                HttpClient client = new HttpClient();
    
                // Make a list of web addresses.
                List<string> urlList = SetUpURLList();
    
                var total = 0;
                var position = 0;
    
                foreach (var url in urlList)
                {
                    // GetByteArrayAsync returns a task. At completion, the task
                    // produces a byte array.
                    byte[] urlContents = await client.GetByteArrayAsync(url);
    
                    DisplayResults(url, urlContents, ++position);
    
                    // Update the total.
                    total += urlContents.Length;
                }
    
                // Display the total count for all of the websites.
                ResultsTextBox.Text +=
                    $"\r\n\r\nTOTAL bytes returned:  {total}\r\n";
            }
    
            private List<string> SetUpURLList()
            {
                List<string> urls = new List<string>
                {
                    "https://msdn.microsoft.com/library/hh191443.aspx",
                    "https://msdn.microsoft.com/library/aa578028.aspx",
                    "https://msdn.microsoft.com/library/jj155761.aspx",
                    "https://msdn.microsoft.com/library/hh290140.aspx",
                    "https://msdn.microsoft.com/library/hh524395.aspx",
                    "https://msdn.microsoft.com/library/ms404677.aspx",
                    "https://msdn.microsoft.com",
                    "https://msdn.microsoft.com/library/ff730837.aspx"
                };
                return urls;
            }
    
            private void DisplayResults(string url, byte[] content, int pos)
            {
                // Display the length of each website. The string format is designed
                // to be used with a monospaced font, such as Lucida Console or
                // Global Monospace.
    
                // Strip off the "https://".
                var displayURL = url.Replace("https://", "");
                // Display position in the URL list, the URL, and the number of bytes.
                ResultsTextBox.Text += $"\n{pos}. {displayURL,-58} {content.Length,8}";
            }
        }
    }
    
  11. 选择 CTRL+F5 键以运行程序,然后多次选择“开始” 按钮。Choose the CTRL+F5 keys to run the program, and then choose the Start button several times.

  12. 禁用“开始”按钮取消并重启操作运行多个操作并将输出排入队列中进行更改以处理重新进入。Make the changes from Disable the Start Button, Cancel and Restart the Operation, or Run Multiple Operations and Queue the Output to handle the reentrancy.

请参阅See also