【问题标题】:What is the best way to cal API calls in parallel in .net Core, C#?在 .net Core、C# 中并行调用 API 调用的最佳方法是什么?
【发布时间】:2019-10-11 21:51:07
【问题描述】:

我想并行调用我的 API x 次,以便可以快速完成处理。 我有以下三种方法,我必须并行调用 API。我正在尝试了解执行此操作的最佳方式。

基本代码

var client = new System.Net.Http.HttpClient();
client.DefaultRequestHeaders.Add("Accept", "application/json");

client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com");
var list = new List<int>();

var listResults = new List<string>();
for (int i = 1; i < 5; i++)
{
    list.Add(i);
}

使用 Parallel.ForEach 的第一种方法

Parallel.ForEach(list,new ParallelOptions() { MaxDegreeOfParallelism = 3 }, index =>
{
    var response = client.GetAsync("posts/" + index).Result;

    var contents =  response.Content.ReadAsStringAsync().Result;
    listResults.Add(contents);
    Console.WriteLine(contents);
});

Console.WriteLine("After all parallel tasks are done with Parallel for each");

任务的第二种方法。我不确定这是否并行运行。如果有请告诉我

var loadPosts = new List<Task<string>>();
foreach(var post in list)
{
    var response = await client.GetAsync("posts/" + post);

    var contents = response.Content.ReadAsStringAsync();
    loadPosts.Add(contents);
    Console.WriteLine(contents.Result);
}

await Task.WhenAll(loadPosts);

Console.WriteLine("After all parallel tasks are done with Task When All");

使用动作块的第三种方法 - 这是我认为我应该一直做的,但我想听听来自社区的意见

var responses = new List<string>();

var block = new ActionBlock<int>(
    async x => {
        var response = await client.GetAsync("posts/" + x);
        var contents = await response.Content.ReadAsStringAsync();
        Console.WriteLine(contents);
        responses.Add(contents);                
    },
    new ExecutionDataflowBlockOptions
    {
        MaxDegreeOfParallelism = 6, // Parallelize on all cores
    });

for (int i = 1; i < 5; i++)
{
    block.Post(i);
}

block.Complete();
await block.Completion;

Console.WriteLine("After all parallel tasks are done with Action block");

【问题讨论】:

    标签: c# asp.net-core parallel-processing task-parallel-library


    【解决方案1】:

    方法 2 接近。这是一条经验法则:I/O 绑定操作 => 使用 Tasks/WhenAll(异步),计算绑定操作 => 使用并行。 Http 请求是网络 I/O。

                foreach (var post in list)
                {
                    async Task<string> func()
                    {
                        var response = await client.GetAsync("posts/" + post);
                        return await response.Content.ReadAsStringAsync();
                    }
    
                    tasks.Add(func());
                }
    
                await Task.WhenAll(tasks);
    
                var postResponses = new List<string>();
    
                foreach (var t in tasks) {
                    var postResponse = await t; //t.Result would be okay too.
                    postResponses.Add(postResponse);
                    Console.WriteLine(postResponse);
                }
    

    【讨论】:

    • @Yulli,但这不是并行运行的。我们不是在等待每个响应返回来调用循环中的下一个响应吗?我认为 Action 块最适合用于 Http 请求和并行 foreach 用于计算操作?
    • 请注意在 func() 调用之前没有等待。因此,此代码不会等待第一个 forloop 内的任何任务。本地函数正在返回一个任务。任务集合是任务的集合,可能完成也可能不完成。 awaitWhenAll 创建了一个在任务中的所有任务对象都完成后执行的延续。 Async/await/WhenAll 是关于异步而不是并行。您的困惑可能与本地功能有关。 async Task func() 是函数定义,不是函数调用。
    • @LearnAspNet 当调用 tasks.Add(func()) 时,任务会立即启动并返回存储在任务列表中的令牌(即任务)。
    • @YuliBonner 如果我想包含要使用的核心数量怎么办。同样在func中,如果你有await client.GetAsync,难道不等待操作完成后才能将下一个任务添加到任务列表/for循环继续吗?
    • 就内核而言,您不会从使用额外内核中获得任何好处。触发一个 http 请求需要很少的 cpu。所有的处理都发生在服务器上。任务可以使用 ThreadPool 中的多个线程,但在这种情况下,它几乎肯定会重用同一个线程,因为很少有处理发生。就 func 而言,不等待对 func 的调用。所有 http 请求都将同时未完成。传入WhenAll时,tasks中的所有Tasks都在运行。
    【解决方案2】:

    我制作了一个小控制台应用程序来测试 ping API“https://jsonplaceholder.typicode.com/todos/{i}”的所有方法 200 次。 @MikeLimaSierra 方法 1 或 3 是最快的!

    Method DegreeOfParallelism Time
    Not Parallel n/a 8.4 sec
    @LearnAspNet (OP) Method 1 2 5.494 sec
    @LearnAspNet (OP) Method 1 30 1.235 sec
    @LearnAspNet (OP) Method 3 2 4.750 sec
    @LearnAspNet (OP) Method 3 30 1.795 sec
    @jamespconnor Method n/a 21.5 sec
    @YuliBonner Method n/a 21.4 sec

    【讨论】:

    • 我没有看到 MikeLimaSierra 的任何方法,所以你的结果很混乱。你到底测试了什么?你有那个来源吗?
    • 更正:我的意思是 OP (@LearnAspNet)...我猜 OP 是由 MikeLimaSierra 编辑的
    【解决方案3】:

    我会使用以下,它无法控制并发(它会并行调度所有 HTTP 请求,与您的第三种方法不同)但它要简单得多 - 它只有一个 await

    var client = new HttpClient();
    var list = new[] { 1, 2, 3, 4, 5 };
    var postTasks = list.Select(p => client.GetStringAsync("posts/" + p));
    var posts = await Task.WhenAll(postTasks);
    foreach (var postContent in posts)
    {
        Console.WriteLine(postContent);
    }
    

    【讨论】:

    • 我的第三种方法将同时调度所有内容
    • 您的第三种方法不会同时发送所有内容(在您的情况下,它只会在您的物品少于 6 件时才会这样做) - 您设置了 MaxDegreeOfParallelism = 6 所以它只会有一个一次最多飞行 6 件物品。
    • 是的,这就是我的意思,它会同时发送 6 个请求。你曾经使用过 Action 块还是只使用 Task.WhenAll?
    • 我喜欢你的代码 sn-p 因为它让事情变得简单,但你的代码有一个非常微妙的问题:Task.WhenAll(IEnumerable) 允许在枚举中枚举多次,这意味着每次重新启动枚举器时,Linq 查询都会重新运行,发出比预期更多的 HttpRequest。作为一个简单的修复,您应该在将 postTasks 传递给 Task.WhenAll 之前将其转换为数组(或列表):var posts = await Task.WhenAll(postTasks).ToArray();
    • @ClaudiuGuiman - 你提出了一个很好的观点,但你有 Task.WhenAll(IEnumerable&lt;Task&gt;) is allowed to enumerate several times over the enumeration 的来源吗?这篇文章(@stephen-cleary - 以他的 TPL 帖子/知识而闻名)似乎与以下内容相矛盾:stackoverflow.com/a/43762566/1238322
    猜你喜欢
    • 2016-05-08
    • 1970-01-01
    • 1970-01-01
    • 2010-09-22
    • 1970-01-01
    • 2022-11-04
    • 2020-04-13
    • 2019-09-26
    • 2019-12-18
    相关资源
    最近更新 更多