Практическое руководство. Расширение пошагового руководства по асинхронным процедурам с использованием метода Task.WhenAll (C#)How to extend the async walkthrough by using Task.WhenAll (C#)

Можно повысить производительность асинхронного решения, которое описывается в пошаговом руководстве по получению доступа к Интернету с помощью модификатора Async и оператора Await (C#), с помощью метода Task.WhenAll.You can improve the performance of the async solution in Walkthrough: Accessing the Web by Using async and await (C#) by using the Task.WhenAll method. Этот метод асинхронно ожидает несколько асинхронных операций, которые представлены в виде коллекции задач.This method asynchronously awaits multiple asynchronous operations, which are represented as a collection of tasks.

Как вы могли заметить в этом пошаговом руководстве, веб-сайты загружаются с разной скоростью.You might have noticed in the walkthrough that the websites download at different rates. Иногда один из веб-сайтов работает слишком медленно, что задерживает все остальные загрузки.Sometimes one of the websites is very slow, which delays all the remaining downloads. Во время работы асинхронных программ, созданных в этом пошаговом руководстве, программу можно с легкостью завершить, если нет необходимости в ожидании, но было бы лучше начать все загрузки одновременно и дать возможность более быстрым загрузкам продолжаться без ожидания более медленных.When you run the asynchronous solutions that you build in the walkthrough, you can end the program easily if you don't want to wait, but a better option would be to start all the downloads at the same time and let faster downloads continue without waiting for the one that’s delayed.

Метод Task.WhenAll можно применить к коллекции задач.You apply the Task.WhenAll method to a collection of tasks. Метод WhenAll, примененный к коллекции, возвращает одну задачу, которая остается незавершенной до тех пор, пока не будет выполнена каждая задача из коллекции.The application of WhenAll returns a single task that isn’t complete until every task in the collection is completed. Как видим, задачи выполняются параллельно, однако дополнительные потоки не создаются.The tasks appear to run in parallel, but no additional threads are created. Задачи могут выполняться в любом порядке.The tasks can complete in any order.

Важно!

Следующие процедуры описывают расширения для асинхронных приложений, разработка которых описывается в статье Пошаговое руководство. Получение доступа к Интернету с помощью модификатора Async и оператора Await.The following procedures describe extensions to the async applications that are developed in Walkthrough: Accessing the Web by Using async and await (C#). Вы можете разработать приложения, выполнив пошаговое руководство или скачав код на странице Примеры кода от разработчиков.You can develop the applications by either completing the walkthrough or downloading the code from Developer Code Samples.

Для выполнения этого примера на компьютере должна быть установлена среда Visual Studio 2012 или более поздней версии.To run the example, you must have Visual Studio 2012 or later installed on your computer.

Добавление метода Task.WhenAll в решение GetURLContentsAsyncTo add Task.WhenAll to your GetURLContentsAsync solution

  1. Добавьте метод ProcessURLAsync в первое приложение, которое разрабатывается в статье Пошаговое руководство. Получение доступа к Интернету с помощью модификатора Async и оператора Await.Add the ProcessURLAsync method to the first application that's developed in Walkthrough: Accessing the Web by Using async and await (C#).

    • Если вы скачали код со страницы Примеры кода от разработчиков, откройте проект AsyncWalkthrough, а затем добавьте ProcessURLAsync в файл MainWindow.xaml.cs.If you downloaded the code from Developer Code Samples, open the AsyncWalkthrough project, and then add ProcessURLAsync to the MainWindow.xaml.cs file.

    • Если вы разработали код, выполнив пошаговое руководство, добавьте ProcessURLAsync в приложение, которое включает метод GetURLContentsAsync.If you developed the code by completing the walkthrough, add ProcessURLAsync to the application that includes the GetURLContentsAsync method. Файл MainWindow.xaml.cs для этого приложения — это первый пример в разделе "Полный код примеров из пошагового руководства".The MainWindow.xaml.cs file for this application is the first example in the "Complete Code Examples from the Walkthrough" section.

    Метод ProcessURLAsync объединяет действия в теле цикла foreach в SumPageSizesAsync в исходном пошаговом руководстве.The ProcessURLAsync method consolidates the actions in the body of the foreach loop in SumPageSizesAsync in the original walkthrough. Метод асинхронно скачивает содержимое указанного веб-сайта в виде массива байтов и затем отображает и возвращает длину массива байтов.The method asynchronously downloads the contents of a specified website as a byte array, and then displays and returns the length of the byte array.

    private async Task<int> ProcessURLAsync(string url)
    {
        var byteArray = await GetURLContentsAsync(url);
        DisplayResults(url, byteArray);
        return byteArray.Length;
    }
    
  2. Закомментируйте или удалите цикл foreach в SumPageSizesAsync, как показано в следующем коде.Comment out or delete the foreach loop in SumPageSizesAsync, as the following code shows.

    //var total = 0;
    //foreach (var url in urlList)
    //{
    //    byte[] urlContents = await GetURLContentsAsync(url);
    
    //    // The previous line abbreviates the following two assignment statements.
    //    // GetURLContentsAsync returns a Task<T>. At completion, the task
    //    // produces a byte array.
    //    //Task<byte[]> getContentsTask = GetURLContentsAsync(url);
    //    //byte[] urlContents = await getContentsTask;
    
    //    DisplayResults(url, urlContents);
    
    //    // Update the total.
    //    total += urlContents.Length;
    //}
    
  3. Создайте коллекцию задач.Create a collection of tasks. Следующий код определяет запрос, который при выполнении метода ToArray создает коллекцию задач, скачивающих содержимое каждого веб-сайта.The following code defines a query that, when executed by the ToArray method, creates a collection of tasks that download the contents of each website. Задачи запускаются при вычислении запроса.The tasks are started when the query is evaluated.

    Добавьте следующий код в метод SumPageSizesAsync после объявления urlList.Add the following code to method SumPageSizesAsync after the declaration of urlList.

    // Create a query.
    IEnumerable<Task<int>> downloadTasksQuery =
        from url in urlList select ProcessURLAsync(url);
    
    // Use ToArray to execute the query and start the download tasks.
    Task<int>[] downloadTasks = downloadTasksQuery.ToArray();
    
  4. Примените Task.WhenAll к коллекции задач, downloadTasks.Apply Task.WhenAll to the collection of tasks, downloadTasks. Task.WhenAll возвращает одну задачу, которая завершается после завершения всех задач в коллекции задач.Task.WhenAll returns a single task that finishes when all the tasks in the collection of tasks have completed.

    В следующем примере выражение await ожидает завершения одной задачи, возвращаемой WhenAll.In the following example, the await expression awaits the completion of the single task that WhenAll returns. Результат этого выражения – массив целых чисел, каждое из которых – размер загруженного веб-сайта.The expression evaluates to an array of integers, where each integer is the length of a downloaded website. Добавьте следующий код в SumPageSizesAsync сразу после кода, добавленного на предыдущем шаге.Add the following code to SumPageSizesAsync, just after the code that you added in the previous step.

    // Await the completion of all the running tasks.
    int[] lengths = await Task.WhenAll(downloadTasks);
    
    //// The previous line is equivalent to the following two statements.
    //Task<int[]> whenAllTask = Task.WhenAll(downloadTasks);
    //int[] lengths = await whenAllTask;
    
  5. И, наконец, используйте метод Sum для вычисления суммы длин всех веб-сайтов.Finally, use the Sum method to calculate the sum of the lengths of all the websites. Добавьте следующую строку в SumPageSizesAsync.Add the following line to SumPageSizesAsync.

    int total = lengths.Sum();
    

Добавление метода Task.WhenAll в решение HttpClient.GetByteArrayAsyncTo add Task.WhenAll to the HttpClient.GetByteArrayAsync solution

  1. Добавьте следующую версию метода ProcessURLAsync во второе приложение, которое разрабатывается в статье Пошаговое руководство. Получение доступа к Интернету с помощью модификатора Async и оператора Await.Add the following version of ProcessURLAsync to the second application that's developed in Walkthrough: Accessing the Web by Using async and await (C#).

    • Если вы скачали код со страницы Примеры кода от разработчиков, откройте проект AsyncWalkthrough_HttpClient, а затем добавьте ProcessURLAsync в файл MainWindow.xaml.cs.If you downloaded the code from Developer Code Samples, open the AsyncWalkthrough_HttpClient project, and then add ProcessURLAsync to the MainWindow.xaml.cs file.

    • Если вы разработали код, выполнив пошаговое руководство, добавьте ProcessURLAsync в приложение, которое использует метод HttpClient.GetByteArrayAsync.If you developed the code by completing the walkthrough, add ProcessURLAsync to the application that uses the HttpClient.GetByteArrayAsync method. Файл MainWindow.xaml.cs для этого приложения  — это второй пример в разделе "Полный код примеров из пошагового руководства".The MainWindow.xaml.cs file for this application is the second example in the "Complete Code Examples from the Walkthrough" section.

    Метод ProcessURLAsync объединяет действия в теле цикла foreach в SumPageSizesAsync в исходном пошаговом руководстве.The ProcessURLAsync method consolidates the actions in the body of the foreach loop in SumPageSizesAsync in the original walkthrough. Метод асинхронно скачивает содержимое указанного веб-сайта в виде массива байтов и затем отображает и возвращает длину массива байтов.The method asynchronously downloads the contents of a specified website as a byte array, and then displays and returns the length of the byte array.

    Единственное отличие от метода ProcessURLAsync из предыдущей процедуры заключается в использовании экземпляра HttpClient, client.The only difference from the ProcessURLAsync method in the previous procedure is the use of the HttpClient instance, client.

    async Task<int> ProcessURLAsync(string url, HttpClient client)
    {
        byte[] byteArray = await client.GetByteArrayAsync(url);
        DisplayResults(url, byteArray);
        return byteArray.Length;
    }
    
  2. Закомментируйте или удалите цикл For Each или foreach в SumPageSizesAsync, как показано в следующем коде.Comment out or delete the For Each or foreach loop in SumPageSizesAsync, as the following code shows.

    //var total = 0;
    //foreach (var url in urlList)
    //{
    //    // GetByteArrayAsync returns a Task<T>. At completion, the task
    //    // produces a byte array.
    //    byte[] urlContent = await client.GetByteArrayAsync(url);
    
    //    // The previous line abbreviates the following two assignment
    //    // statements.
    //    Task<byte[]> getContentTask = client.GetByteArrayAsync(url);
    //    byte[] urlContent = await getContentTask;
    
    //    DisplayResults(url, urlContent);
    
    //    // Update the total.
    //    total += urlContent.Length;
    //}
    
  3. Определите запрос, который при выполнении метода ToArray создает коллекцию задач, скачивающих содержимое каждого веб-сайта.Define a query that, when executed by the ToArray method, creates a collection of tasks that download the contents of each website. Задачи запускаются при вычислении запроса.The tasks are started when the query is evaluated.

    Добавьте следующий код в метод SumPageSizesAsync после объявления client и urlList.Add the following code to method SumPageSizesAsync after the declaration of client and urlList.

    // Create a query.
    IEnumerable<Task<int>> downloadTasksQuery =
        from url in urlList select ProcessURLAsync(url, client);
    
    // Use ToArray to execute the query and start the download tasks.
    Task<int>[] downloadTasks = downloadTasksQuery.ToArray();
    
  4. Затем примените Task.WhenAll к коллекции задач, downloadTasks.Next, apply Task.WhenAll to the collection of tasks, downloadTasks. Task.WhenAll возвращает одну задачу, которая завершается после завершения всех задач в коллекции задач.Task.WhenAll returns a single task that finishes when all the tasks in the collection of tasks have completed.

    В следующем примере выражение await ожидает завершения одной задачи, возвращаемой WhenAll.In the following example, the await expression awaits the completion of the single task that WhenAll returns. Результат выражения await — массив целых чисел, каждое из которых представляет размер скачанного веб-сайта.When complete, the await expression evaluates to an array of integers, where each integer is the length of a downloaded website. Добавьте следующий код в SumPageSizesAsync сразу после кода, добавленного на предыдущем шаге.Add the following code to SumPageSizesAsync, just after the code that you added in the previous step.

    // Await the completion of all the running tasks.
    int[] lengths = await Task.WhenAll(downloadTasks);
    
    //// The previous line is equivalent to the following two statements.
    //Task<int[]> whenAllTask = Task.WhenAll(downloadTasks);
    //int[] lengths = await whenAllTask;
    
  5. И, наконец, используйте метод Sum для получения суммы длин всех веб-сайтов.Finally, use the Sum method to get the sum of the lengths of all the websites. Добавьте следующую строку в SumPageSizesAsync.Add the following line to SumPageSizesAsync.

    int total = lengths.Sum();
    

Тестирование решений Task.WhenAllTo test the Task.WhenAll solutions

ПримерExample

В следующем коде показано расширение для проекта, использующее метод GetURLContentsAsync для скачивания содержимого из Интернета.The following code shows the extensions to the project that uses the GetURLContentsAsync method to download content from the web.

// Add the following using directives, and add a reference for System.Net.Http.
using System.Net.Http;
using System.IO;
using System.Net;

namespace AsyncExampleWPF_WhenAll
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private async void startButton_Click(object sender, RoutedEventArgs e)
        {
            resultsTextBox.Clear();

            // Two-step async call.
            Task sumTask = SumPageSizesAsync();
            await sumTask;

            // One-step async call.
            //await SumPageSizesAsync();

            resultsTextBox.Text += "\r\nControl returned to startButton_Click.\r\n";
        }

        private async Task SumPageSizesAsync()
        {
            // Make a list of web addresses.
            List<string> urlList = SetUpURLList();

            // Create a query.
            IEnumerable<Task<int>> downloadTasksQuery =
                from url in urlList select ProcessURLAsync(url);

            // Use ToArray to execute the query and start the download tasks.
            Task<int>[] downloadTasks = downloadTasksQuery.ToArray();

            // You can do other work here before awaiting.

            // Await the completion of all the running tasks.
            int[] lengths = await Task.WhenAll(downloadTasks);

            //// The previous line is equivalent to the following two statements.
            //Task<int[]> whenAllTask = Task.WhenAll(downloadTasks);
            //int[] lengths = await whenAllTask;

            int total = lengths.Sum();

            //var total = 0;
            //foreach (var url in urlList)
            //{
            //    byte[] urlContents = await GetURLContentsAsync(url);

            //    // The previous line abbreviates the following two assignment statements.
            //    // GetURLContentsAsync returns a Task<T>. At completion, the task
            //    // produces a byte array.
            //    //Task<byte[]> getContentsTask = GetURLContentsAsync(url);
            //    //byte[] urlContents = await getContentsTask;

            //    DisplayResults(url, urlContents);

            //    // 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",
                "https://msdn.microsoft.com/library/windows/apps/br211380.aspx",
                "https://msdn.microsoft.com/library/hh290136.aspx",
                "https://msdn.microsoft.com/library/ee256749.aspx",
                "https://msdn.microsoft.com/library/hh290138.aspx",
                "https://msdn.microsoft.com/library/hh290140.aspx",
                "https://msdn.microsoft.com/library/dd470362.aspx",
                "https://msdn.microsoft.com/library/aa578028.aspx",
                "https://msdn.microsoft.com/library/ms404677.aspx",
                "https://msdn.microsoft.com/library/ff730837.aspx"
            };
            return urls;
        }

        // The actions from the foreach loop are moved to this async method.
        private async Task<int> ProcessURLAsync(string url)
        {
            var byteArray = await GetURLContentsAsync(url);
            DisplayResults(url, byteArray);
            return byteArray.Length;
        }

        private async Task<byte[]> GetURLContentsAsync(string url)
        {
            // The downloaded resource ends up in the variable named content.
            var content = new MemoryStream();

            // Initialize an HttpWebRequest for the current URL.
            var webReq = (HttpWebRequest)WebRequest.Create(url);

            // Send the request to the Internet resource and wait for
            // the response.
            using (WebResponse response = await webReq.GetResponseAsync())
            {
                // Get the data stream that is associated with the specified url.
                using (Stream responseStream = response.GetResponseStream())
                {
                    await responseStream.CopyToAsync(content);
                }
            }

            // Return the result as a byte array.
            return content.ToArray();

        }

        private void DisplayResults(string url, byte[] content)
        {
            // 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.
            var bytes = content.Length;
            // Strip off the "https://".
            var displayURL = url.Replace("https://", "");
            resultsTextBox.Text += $"\n{displayURL,-58} {bytes,8}";
        }
    }
}

ПримерExample

В следующем коде показано расширение для проекта, использующее метод HttpClient.GetByteArrayAsync для скачивания содержимого из Интернета.The following code shows the extensions to the project that uses method HttpClient.GetByteArrayAsync to download content from the web.

// Add the following using directives, and add a reference for System.Net.Http.
using System.Net.Http;
using System.IO;
using System.Net;

namespace AsyncExampleWPF_HttpClient_WhenAll
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private async void startButton_Click(object sender, RoutedEventArgs e)
        {
            resultsTextBox.Clear();

            // One-step async call.
            await SumPageSizesAsync();

            // Two-step async call.
            //Task sumTask = SumPageSizesAsync();
            //await sumTask;

            resultsTextBox.Text += "\r\nControl returned to startButton_Click.\r\n";
        }

        private async Task SumPageSizesAsync()
        {
            // Make a list of web addresses.
            List<string> urlList = SetUpURLList();

            // Declare an HttpClient object and increase the buffer size. The
            // default buffer size is 65,536.
            HttpClient client = new HttpClient() { MaxResponseContentBufferSize = 1000000 };

            // Create a query.
            IEnumerable<Task<int>> downloadTasksQuery =
                from url in urlList select ProcessURLAsync(url, client);

            // Use ToArray to execute the query and start the download tasks.
            Task<int>[] downloadTasks = downloadTasksQuery.ToArray();

            // You can do other work here before awaiting.

            // Await the completion of all the running tasks.
            int[] lengths = await Task.WhenAll(downloadTasks);

            //// The previous line is equivalent to the following two statements.
            //Task<int[]> whenAllTask = Task.WhenAll(downloadTasks);
            //int[] lengths = await whenAllTask;

            int total = lengths.Sum();

            //var total = 0;
            //foreach (var url in urlList)
            //{
            //    // GetByteArrayAsync returns a Task<T>. At completion, the task
            //    // produces a byte array.
            //    byte[] urlContent = await client.GetByteArrayAsync(url);

            //    // The previous line abbreviates the following two assignment
            //    // statements.
            //    Task<byte[]> getContentTask = client.GetByteArrayAsync(url);
            //    byte[] urlContent = await getContentTask;

            //    DisplayResults(url, urlContent);

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

            // Display the total count for all of the web addresses.
            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",
                "https://msdn.microsoft.com/library/hh290136.aspx",
                "https://msdn.microsoft.com/library/ee256749.aspx",
                "https://msdn.microsoft.com/library/hh290138.aspx",
                "https://msdn.microsoft.com/library/hh290140.aspx",
                "https://msdn.microsoft.com/library/dd470362.aspx",
                "https://msdn.microsoft.com/library/aa578028.aspx",
                "https://msdn.microsoft.com/library/ms404677.aspx",
                "https://msdn.microsoft.com/library/ff730837.aspx"
            };
            return urls;
        }

        // The actions from the foreach loop are moved to this async method.
        async Task<int> ProcessURLAsync(string url, HttpClient client)
        {
            byte[] byteArray = await client.GetByteArrayAsync(url);
            DisplayResults(url, byteArray);
            return byteArray.Length;
        }

        private void DisplayResults(string url, byte[] content)
        {
            // Display the length of each web site. The string format
            // is designed to be used with a monospaced font, such as
            // Lucida Console or Global Monospace.
            var bytes = content.Length;
            // Strip off the "https://".
            var displayURL = url.Replace("https://", "");
            resultsTextBox.Text += $"\n{displayURL,-58} {bytes,8}";
        }
    }
}

См. также разделSee also