如何在ForEach循环中使用async/await发送http请求

3

以下代码是一个 程序。

  • payloadList 包含像 {"id": 1, "foo": "one" } 这样的 json 对象。
  • 每个 payloadList 的对象都应该使用 httpClient.SendAsync() 发送到服务器。
  • 每个请求的 response 应该存储在 responseList 中。

下面的代码部分工作。但我不明白为什么有些部分不起作用。我猜测当执行 responseList.Add(foo) 时,响应还没有完成。

对于每个 json 对象 {"id": 1, "foo": "one" },都应该发送此请求。

public static async Task<string> DoRequest(HttpClient client, string payload)
{   
    var request = new HttpRequestMessage(HttpMethod.Post, 
                           "http://httpbin.org/anything");
    request.Content = new StringContent(payload
                           , Encoding.UTF8, "application/json");        
    var response = await client.SendAsync(request); 
    string responseContent = await response.Content.ReadAsStringAsync(); 
    return responseContent;
}

DoRequest()方法封装了请求,可以在主函数内像这样使用

static async Task Main()
{
    var responseList = new List<string>();  
    var payloadList = new List<string>{"{ 'id': 1, 'bar': 'One'}",
                                       "{ 'id': 2, 'bar': 'Two'}",
                                       "{ 'id': 3, 'bar': 'Three'}"};
        
    var client = new HttpClient();
    
    payloadList.ForEach(async (payload) => {
        var responseFoo = await DoRequest(client, payload);
        responseFoo.Dump("response"); // contains responseFoo
        responseList.Add(responseFoo);  // adding responseFoo to list fails
    });                     
    responseList.Dump();    // is empty
}

responseList为空。

  • 预期responseList.Dump()包含所有的响应responseFoo
  • 实际上responseList是空的。

Linqpad-demo

问题

  • 如何将每个await client.SendAsync(request)的响应添加到responseList中?
  • 为什么responseList为空,尽管foo.Dump()有效?
  • 如何确认或检查每个client.SendAsync是否已完成?
  • 您会以不同的方式编写上面的代码吗?为什么?

请逐个问题提问。另请参阅[ask]。 - ADyson
responseList.Add(foo); // 这个不起作用的方式是什么?有错误吗? - ADyson
responseList为空。预期responseList.Dump()包含所有响应foo。实际上,responseList为空。 - surfmuggle
PS:将 responseList.Dump(); 放在 foreach 循环外显然行不通,因为它会在所有异步操作完成之前运行。 - ADyson
运行多个异步任务并等待它们全部完成可能会有所帮助。 - ADyson
显示剩余2条评论
1个回答

4

List.ForEach 不支持 Task,会并行执行所有操作而不等待结果(即它将为 payloadList 中的所有项创建任务,并继续执行下一条语句 responseList.Dump(); 而不等待它们)。

在较新版本的.NET中,您可以使用Parallel.ForEachAsync(例如在this answer中)与适当的System.Collections.Concurrent集合结合使用,例如ConcurrentBag<T>List不是线程安全的,同时修改它可能会导致很多问题。
代码如下所示:
``` var responseList = new ConcurrentBag<string>();
await Parallel.ForEachAsync(payloadList, async (payload, ct) => { var foo = await DoRequest(client, payload, ct); responseList.Add(foo); });
static async Task<string> DoRequest(HttpClient client, string payload, CancellationToken ct) { var request = new HttpRequestMessage(HttpMethod.Post, "http://httpbin.org/anything"); request.Content = new StringContent(payload, Encoding.UTF8, "application/json"); var response = await client.SendAsync(request, ct); string responseContent = await response.Content.ReadAsStringAsync(ct); return responseContent; } ```
如果您希望所有请求都并行运行-只需创建一个任务可枚举对象并使用Task.WhenAll<T>()
``` var tsks = payloadList .Select(payload => DoRequest(client, payload));
string[] result = await Task.WhenAll(tsks); ```
如果您想要依次执行请求-只需切换到普通的foreach
``` var responseList = new List<string>(); // 在这种情况下不需要并发集合
foreach (var payload in payloadList) { var foo = await DoRequest(client, payload); responseList.Add(foo); } ```

如果你想深入了解,这里有一些链接:


确保使用 Parallel.ForEachAsync 时要使用 await - apc
您能添加一些代码说明如何使用它吗? - surfmuggle
谢谢你提供的代码。还有一件事。我本来以为 responseList.Add(foo) 应该可以工作,但它却是空的。这是为什么呢? - surfmuggle
1
@surfmuggle 可能你的应用在请求完成之前就已经结束了。而且 responseList.Dump(); 很有可能在任何请求完成之前就被执行了。也就是说,在 List.ForEach 中使用 async-await 是没有效果的。 - Guru Stron
1
@surfmuggle添加了一些链接,如果您想深入了解这个主题,它们可能会有用。 - Guru Stron
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接