Обработка повторного входа в асинхронных приложениях (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 .NET Framework 4.5 or newer installed on your computer.

Примечание

Версия протокола TLS 1.2 теперь является минимальной версией для использования в разработке приложений.Transport Layer Security (TLS) version 1.2 is now the minimum version to use in your app development. Если приложение предназначено для более ранней версии .NET Framework, чем 4.7, обратитесь к следующей статье, чтобы ознакомиться с рекомендациями по протоколу TLS в .NET Framework.If your app targets a .NET Framework version earlier than 4.7, refer to the following article for Transport Layer Security (TLS) best practices with the .NET Framework.

Распознавание поддержки повторного входаRecognizing Reentrancy

В примере в этом разделе пользователь нажимает кнопку Start, чтобы инициировать асинхронное приложение, загружающее ряд веб-сайтов и вычисляющее общее число загруженных байтов.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. Синхронная версия примера реагирует одинаково, независимо от того, сколько раз пользователь нажмет кнопку, поскольку после первого раза поток пользовательского интерфейса игнорирует эти события до завершения выполнения приложения.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. При этом в асинхронном приложении поток пользовательского интерфейса продолжает реагировать, и можно ввести асинхронную операцию повторно до ее завершения.In an asynchronous app, however, the UI thread continues to respond, and you might reenter the asynchronous operation before it has completed.

В следующем примере показаны ожидаемые выходные данные, если пользователь нажимает кнопку Start только один раз.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

Можно заблокировать кнопку Start во время операции, отключив кнопку в верхней части обработчика событий 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. Также можно загрузить готовое приложение в разделе Async Samples: Reentrancy in .NET Desktop Apps.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

Вместо отключения кнопки Start можно оставить кнопку активной, но при этом, если пользователь нажмет эту кнопку еще раз, нужно отменить операцию, которая уже выполняется, и задать продолжение выполнения последней запущенной операции.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. Также можно загрузить готовое приложение в разделе Async Samples: Reentrancy in .NET Desktop Apps.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";
}

При многократном нажатии кнопки Start во время выполнения этого приложения код должен создать результаты, похожие на следующие выходные данные.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

Третий пример является наиболее сложным, так как приложение запускает другую асинхронную операцию каждый раз, когда пользователь нажимает кнопку Start, и все операции выполняются до завершения.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. Также можно загрузить готовое приложение в разделе Async Samples: Reentrancy in .NET Desktop Apps.You also can download the finished app from Async Samples: Reentrancy in .NET Desktop Apps. Этот проект называется QueueResults.The name of the project is QueueResults.

Ниже показаны выходные данные, отображаемые в результате нажатия кнопки Start только один раз.The following output shows the result if the user chooses the Start button only once. Метка A указывает, что это результат первого нажатия кнопки Start.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.

Если пользователь нажимает кнопку Start три раза, приложение выдает результат, похожий на приведенный ниже.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 увеличивает букву группы каждый раз, когда пользователь нажимает кнопку Start.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.";
    }
}

Метод AccessTheWebAsyncThe 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 возвращает задачу, которая назначена pendingWork, в AccessTheWebAsync.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;
}

Метод FinishOneGroupAsyncThe 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
    
  • Задача pendingWork имеет значение NULL при запуске метода FinishOneGroupAsync только для группы A, которая запускается первой.The pendingWork task is null at the start of FinishOneGroupAsync only for group A, which started first. Группа A еще не завершила выражение await, когда она достигает метода FinishOneGroupAsync.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 операция не завершает выражение await до входа операции в метод FinishOneGroupAsync.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 на компьютере должны быть установлены 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. Скачайте сжатый файл в разделе Async Samples: Reentrancy in .NET Desktop Apps.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. В обозревателе решений откройте контекстное меню нужного проекта и выберите пункт Назначить запускаемым проектом.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.

Построение приложения WPFTo 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, выберите .NET Framework версии 4.6 или более поздней, а затем нажмите кнопку ОК.Name the project WebsiteDownloadWPF, choose .NET Framework version of 4.6 or higher and then click 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. Замените код в представлении XAML файла MainWindow.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. В обозревателе решений щелкните правой кнопкой мыши Ссылки и выберите Добавить ссылку.In Solution Explorer, right-click on References and select Add Reference.

    Добавьте ссылку на System.Net.Http, если она еще не выбрана.Add a reference for System.Net.Http, if it is not selected already.

  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()
            {
                System.Net.ServicePointManager.SecurityProtocol |= System.Net.SecurityProtocolType.Tls12;
                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, чтобы запустить программу, а затем несколько раз нажмите кнопку Start.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