在 ASP.NET Core 中一次生成 100-500 个任务(每个 HTTP 请求)是一种不好的做法吗? [关闭]

Posted

技术标签:

【中文标题】在 ASP.NET Core 中一次生成 100-500 个任务(每个 HTTP 请求)是一种不好的做法吗? [关闭]【英文标题】:Is spawning 100-500 task at once (per HTTP request) a bad practice in ASP.NET Core? [closed] 【发布时间】:2021-12-11 16:15:40 【问题描述】:

在我的 ASP.NET Core 控制器中,我有一个如下所示的 Action:

public async Task<ActionResult> SomeAction(int someId)

    int[] itemIds = await _service.GetSomeItems(someId);
    var model = new List<ItemDetail>(itemIds.Length);

    foreach (int id in itemIds)
        model.Add(await _service.GetItemDetails(id));
    return View(model);

它可以工作,但速度很慢。我想通过生成多个并行任务来提高“SomeAction”的性能:

public async Task<ActionResult> SomeAction(int someId)

    int[] itemIds = await _service.GetSomeItems(someId);
    var tasks = new List<Task>();

    foreach (int id in itemIds)
        tasks.Add(_service.GetItemDetails(id));
    var model = await Task.WhenAll(tasks);

    return View(model);

这种方法效果更好,但我不确定这种方法是否是一种好的做法。通常 _service.GetSomeItems API 返回 20-30 个元素,但有时它可能返回 100-200 个元素。 在我的情况下,_service 是第 3 方 REST API,延迟为 100-150 毫秒。

我的问题是 - 我一次可以生成多少个任务?如果同时出现多个请求(每个请求将产生 20-200 个任务),我的代码会导致 ThreadPool 饥饿吗? 我的方法通常是一种不好的做法吗?

我要避免的是 ThreadPool 饥饿,在某些情况下可能会导致死锁。

更新: _service.GetItemDetails(id) - 是第 3 方 Web API,请求延迟为 100-150 毫秒。

【问题讨论】:

如果没有看到内部GetItemDetails,我们将无法帮助您。 我们无法评论黑匣子的优缺点 另外,您使用的是IHttpClientFactory吗?或者你是在为每个请求做new HttpClient() 吗?您可能会成为套接字耗尽的受害者 - 如果您正在缓存单个 HttpClient,那么您将拥有一个陈旧的 DNS 缓存。 一般来说,在网站中,您不想考虑并行运行代码 - 由于站点正在服务的其他请求,您已经对并行活动有大量需求。除非您实际上正在开发专用于单个用户的应用程序。 您可能会发现这很有用:How to limit the amount of concurrent async I/O operations? 【参考方案1】:

这不是一个好习惯。问题不在于可以创建多少任务,而在于其他服务器在阻止或限制您之前将接受多少请求、网络开销和延迟、您的网络上的压力。您正在进行网络调用,这意味着您的 CPU 无需执行任何操作,它只是等待来自远程服务器的响应。

然后是服务器端的开销:每个请求打开一个连接,执行单个查询,序列化结果。如果服务器的代码写得不好,它可能会崩溃。如果负载均衡器配置不当,所有 1000 个并发调用可能最终都由同一台机器提供服务。这 1000 个请求可能会打开 1000 个相互阻塞的连接。

如果远程 API 允许,请尝试一次请求多个项目。您只需支付一次开销,服务器只需执行一次查询。

否则,您可以尝试同时执行多个连接,但不能同时执行所有连接。最好通过限制并发执行的数量以及可能的频率来限制请求。

在 .NET 6(下一个 LTS 版本,已在生产中支持)中,您可以在有限的并行度下使用 Parallel.ForEachAsync

ConcurrentQueue<Something> list=new ConcurrentQueue<Something>(1000);
var options=new ParallelOptions  MaxDegreeOfParallelism = 20;

await Parallel.ForEachAsync(itemIds,options,async id=>
    var result=_service.GetItemDetails(id);
    var something=CreateSomething(result);
    list.Add(something);   
);

model.Items=list;

return View(model);

另一种选择是创建使用 Channels 或 TPL Dataflow 块来创建同时处理项目的处理管道,例如第一步生成 ID,下一步检索远程项目,最后一个产生最终结果:

ChannelReader<int> GetSome(int someId,CancellationToken token=default)

    var channel=Channel.CreateUnbounded<int>();
    int[] itemIds = await _service.GetSomeItems(someId);
    foreach(var id in itemIds)
    
        if(token.IsCancellationRequested)
        
            return;
        
        channel.Writer.TryWrite(id);
    
    return channel;    



ChannelReader<Detail> GetDetails(ChannelReader<int> reader,int dop,
    CancellationToken token=default)

    var channel=Channel.CreateUnbounded<Detail>();
    var writer=channel.Writer;

    var options=new ParallelOptions 
        MaxDegreeOfParallelism=dop,
        CancellationToken=token
    ;

    var worker=Parallel.ForEachAsync(reader.ReadAllAsync(token),
        options,
        async id=>
            try 
                var details=_service.GetItemDetails(id);
                await writer.WriteAsync(details);
            
            catch(Exception exc)
            
                //Handle the exception so we can keep processing
                //other messages
            
        );
    worker.ContinueWith(t=>writer.TryComplete(t.Exception));
      
    return channel;    ​


async Task<Model> CreateModel(ChannelReader<Detail> reader,...)

    var allDetails=new List<Detail>(1000);
    await(foreach var detail in reader.ReadAllAsync(token))
    
        allDetails.Add(detail);
        //Do other heavyweight things
    
    var model=new Model Details=allDetails,.....);

这些方法可以链接在管道中:

var chIds=GetSome(123);
var chDetails=GetDetails(chIds,20);
var model=await CreateModel(chDetails);

这是使用 Channels 时的常见模式。在 Go 等语言中,通道用于创建多步处理管道。

将这些方法转换为扩展方法允许以流畅的方式创建管道:

static ChannelReader<Detail> GetDetails(this ChannelReader<int> reader,int dop,CancellationToken token=default)

static async Task<Model> CreateModel(this ChannelReader<Detail> reader,...)


var model= await GetSome(123);
                 .GetDetails(20)
                 .CreateModels();

【讨论】:

以上是关于在 ASP.NET Core 中一次生成 100-500 个任务(每个 HTTP 请求)是一种不好的做法吗? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

ASP.NET Core 1.0 中使用 Swagger 生成文档

[Asp.Net Core]ASP.NET Core WebApi使用Swagger生成api说明文档看这篇就够了

(VIP-朝夕教育)2021-06-19 .NET高级班 56-ASP.NET Core 管道中间件详解

asp.net core web api 生成 swagger 文档

ASP.NET Core MVC 在引用数据库连接时生成异常错误

ASP.NET Core 身份验证 cookie 只收到一次