等待在此函数中做什么

本文关键字:什么 函数 等待 | 更新日期: 2023-09-27 18:37:06

我以为我理解C# async-await模式,但今天我发现我真的不明白。

在像这样的简单代码片段中。我已经System.Net.ServicePointManager.DefaultConnectionLimit = 1000;定义。

public static async Task Test()
{
    HttpClient client = new HttpClient();
    string str;
    for (int i = 1000; i <= 1100; i++)
        str = await client.GetStringAsync($"https://en.wikipedia.org/wiki/{i1}");
}

await在这里做什么?最初我认为,由于这是异步等待模式,这意味着基本上 HttpClient 将以多线程方式启动所有 HTTP GET 调用,即基本上应该一次获取所有 URL。

但是当我使用 Fiddler 来分析行为时,我发现它确实按顺序获取 URL。

我需要将其更改为此以使其工作:

public static async Task<int> Test()
{
    int ret = 0;
    HttpClient client = new HttpClient();
    List<Task> taskList = new List<Task>();
    for (int i = 1000; i <= 1100; i++)
    {
        var i1 = i;
        var task = Task.Run(() => client.GetStringAsync($"https://en.wikipedia.org/wiki/{i1}"));
        taskList.Add(task);
    }
    Task.WaitAll(taskList.ToArray());
    return ret;
}

这次是并行获取 URL。那么await关键字在第一个代码片段中到底做了什么呢?

等待在此函数中做什么

等待 HTTP 请求的完成。代码仅在每个请求完成后恢复(for迭代...)。

您的第二个版本之所以有效,正是因为它启动以下任务之前不会等待每个任务完成,而只会在所有任务启动后等待所有任务完成。

async-await 的用处是允许调用函数在异步函数等待时继续执行其他操作,而不是阻止调用函数直到完成的同步("正常")函数。

await异步等待。它不是阻止调用,并允许方法的调用方继续。await后方法内的其余代码将在返回Task完成后执行。

在代码的第一个版本中,允许调用方继续。但是,循环的每次迭代都将等到GetStringAsync返回的Task完成。这具有按顺序下载每个 URL 的效果,而不是并发下载

请注意,代码的第二个版本不是异步的,因为它使用线程并行执行工作。

如果它是异步的,它将仅使用一个线程检索网页内容,但仍同时检索

像这样的东西(未经测试):

public static async Task<int> Test()
{
    int ret = 0;
    HttpClient client = new HttpClient();
    List<Task> taskList = new List<Task>();
    for (int i = 1000; i <= 1100; i++)
    {
        var i1 = i;
        taskList.Add(client.GetStringAsync($"https://en.wikipedia.org/wiki/{i1}"));
    }
    await Task.WhenAll(taskList.ToArray());
    return ret;
}

在这里,我们异步启动任务并将它们添加到 taskList .这些任务是非阻塞性的,将在下载完成并检索字符串时完成。注意调用Task.WhenAll而不是Task.WaitAll:前者是异步和非阻塞的,后者是同步和阻塞的。这意味着,在await,这个Test()方法的调用者将收到返回的Task<int>:但在下载所有字符串之前,任务将不完整。

这就是迫使async/await在整个堆栈中扩散的原因。一旦最底部的调用是异步的,只有当其他调用方也是异步的时,它才有意义。否则,您将被迫通过Task.Run()调用或类似方式创建线程。

根据 msdn 文档

await 运算符应用于异步方法中的任务,以暂停方法的执行,直到等待的任务完成。该任务代表正在进行的工作。

这意味着 await 运算符会阻止 for 循环的执行,直到它从服务器获得响应,使其按顺序排列。

您可以做的是创建所有任务(以便它开始执行),然后等待所有任务。

这是另一个StackOverflow问题的示例

public IEnumerable<TContent> DownloadContentFromUrls<TContent>(IEnumerable<string> urls)
{
    var queue = new ConcurrentQueue<TContent>();
    using (var client = new HttpClient())
    {
        Task.WaitAll(urls.Select(url =>
        {
            return client.GetAsync(url).ContinueWith(response =>
            {
                var content = JsonConvert.
                    DeserializeObject<IEnumerable<TContent>>(
                        response.Result.Content.ReadAsStringAsync().Result);
                foreach (var c in content)
                    queue.Enqueue(c);
            });
        }).ToArray());
    }
    return queue;
}

msdn 中也有很好的文章解释了如何使用 await 发出并行请求。

编辑:

正如@GaryMcLeanHall注释中指出的那样,您可以将Task.WaitAll更改为await Task.WhenAll并添加 async 修饰符以使方法异步返回

这是另一篇 msdn 文章,它选择了第一篇中的示例并添加了 WhenAll 的使用。