调用 API 时在哪里使用并发

Posted

技术标签:

【中文标题】调用 API 时在哪里使用并发【英文标题】:Where to use concurrency when calling an API 【发布时间】:2015-07-13 08:02:37 【问题描述】:

在 c# 项目中,我正在对 web api 进行一些调用,问题是我在方法的循环中执行它们。通常没有这么多,但即使我正在考虑利用并行性。

到目前为止我正在尝试的是

public void DeployView(int itemId, string itemCode, int environmentTypeId)

    using (var client = new HttpClient())
    
        client.BaseAddress = new Uri(ConfigurationManager.AppSettings["ApiUrl"]);
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        var agents = _agentRepository.GetAgentsByitemId(itemId);

        var tasks = agents.Select(async a =>
            
                var viewPostRequest = new
                    
                        AgentId = a.AgentId,
                        itemCode = itemCode,
                        EnvironmentId = environmentTypeId
                    ;

                var response = await client.PostAsJsonAsync("api/postView", viewPostRequest);
            );

        Task.WhenAll(tasks);
    

但是想知道这是否是正确的路径,或者我应该尝试并行整个 DeployView(即甚至在使用 HttpClient 之前)

现在我看到它发布了,我认为我不能只删除变量响应,只需执行等待而不将其设置为任何变量

谢谢

【问题讨论】:

其实这是一个很好的方向。但是你忘记了最重要的部分。您需要 await 结果,例如await Task.WhenAll 但您需要将“async”关键字添加到您的 DeployView 函数中。您最好深入了解async/await 范式。 那么您面临的问题/异常是什么?我也同意 ckruczek 的观点,你所采取的方向没有错……另外,你想得到回应吗? 我想得到答复,是的。但是如果它们都可以,不知道如何使用它们 "尝试并行整个 DeployItem" 什么意思? @usr,我想知道是否重点是在任务本身中创建 htppclient。抱歉,我放了 DeployItem,它应该是 DeployView 【参考方案1】:

通常不需要并行化请求 - 一个线程发出异步请求就足够了(即使您有数百个请求)。考虑这段代码:

var tasks = agents.Select(a =>
        
            var viewPostRequest = new
                
                    AgentId = a.AgentId,
                    itemCode = itemCode,
                    EnvironmentId = environmentTypeId
                ;

            return client.PostAsJsonAsync("api/postView", viewPostRequest);
        );
    //now tasks is IEnumerable<Task<WebResponse>>
    await Task.WhenAll(tasks);
    //now all the responses are available
    foreach(WebResponse response in tasks.Select(p=> p.Result))
    
        //do something with the response
    

但是,您可以在处理响应时利用并行性。您可以使用上面的“foreach”循环代替:

Parallel.Foreach(tasks.Select(p=> p.Result), response => ProcessResponse(response));

但是TMO,这是异步和并行的最佳利用:

var tasks = agents.Select(async a =>
        
            var viewPostRequest = new
                
                    AgentId = a.AgentId,
                    itemCode = itemCode,
                    EnvironmentId = environmentTypeId
                ;

            var response = await client.PostAsJsonAsync("api/postView", viewPostRequest);
            ProcessResponse(response);
        );
await Task.WhenAll(tasks);   

第一个示例和最后一个示例之间有一个主要区别: 在第一个中,您有一个线程启动异步请求,等待(非阻塞)所有返回,然后才处理它们。 在第二个示例中,您将延续附加到每个任务。这样,每个响应一到达就会得到处理。假设当前的 TaskScheduler 允许并行(多线程)执行任务,则不会像第一个示例中那样保持空闲状态。

*编辑 - 如果您决定并行执行,您可以只使用一个 HttpClient 实例 - 它是线程安全的。

【讨论】:

异步 IO 不会使 IO 更快。它与单个 IO 的速度无关。他只能通过并行化更快。 我从来没有说过异步使 IO 更快。发布单个异步请求相对较快(无需等待响应),速度足以让一个线程立即发出数千个这样的请求。这就是为什么我强调“通常”。【参考方案2】:

您介绍的是并发,而不是并行。更多关于 here.

你的方向很好,虽然我会做一些小改动:

首先,您应该将您的方法标记为async Task,因为您正在使用Task.WhenAll,它返回一个等待对象,您需要异步等待。接下来,您可以简单地从PostAsJsonAsync 返回操作,而不是等待Select 中的每个调用。这将节省一点开销,因为它不会为异步调用生成状态机:

public async Task DeployViewAsync(int itemId, string itemCode, int environmentTypeId)

    using (var client = new HttpClient())
    
        client.BaseAddress = new Uri(ConfigurationManager.AppSettings["ApiUrl"]);
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(
                   new MediaTypeWithQualityHeaderValue("application/json"));

        var agents = _agentRepository.GetAgentsByitemId(itemId);
        var agentTasks = agents.Select(a =>
        
            var viewPostRequest = new
            
                AgentId = a.AgentId,
                itemCode = itemCode,
                EnvironmentId = environmentTypeId
            ;

            return client.PostAsJsonAsync("api/postView", viewPostRequest);
        );

        await Task.WhenAll(agentTasks);
    

HttpClient 能够发出并发请求(有关更多信息,请参阅@usr 链接),因此我看不到每次在您的 lambda 中创建一个新实例的理由。请注意,如果您多次使用DeployViewAsync,也许您希望保留您的HttpClient,而不是每次都分配一个,并在您不再需要它的服务时将其丢弃。

【讨论】:

【参考方案3】:

HttpClient appears to be usable for concurrent requests.这个我自己没有验证过,这只是我从搜索中收集到的。因此,您不必为要开始的每个任务都创建一个新客户端。你可以做你最方便的事情。

一般来说,我努力共享尽可能少的(可变)状态。资源获取通常应向内推向其使用。我认为创建一个助手CreateHttpClient 并在此处为每个请求创建一个新客户端会更好。考虑将 Select 主体设为新的异步方法。然后,HttpClient 的用法对DeployView 完全隐藏。

不要忘记await WhenAll 任务并创建方法async Task。 (如果您不明白为什么有必要这样做,您可以对 await 进行一些研究。)

【讨论】:

感谢您富有洞察力的回答,正如@YuvalItzchakov 所言,我认为在这种情况下保留客户端对我来说更有意义,因为它可用于并发请求,因为此方法将被多次调用

以上是关于调用 API 时在哪里使用并发的主要内容,如果未能解决你的问题,请参考以下文章

Android Studio - 调试 android 应用程序时在哪里可以看到调用堆栈?

使用 fetch API 时在 AJAX 调用期间显示微调器

每次响应 401 Unauthorized 时在 DEV Env 中调用休息,PROD 工作正常

FLutter:迭代列表时在 null 上调用了方法“[]”

使用 redux 构建 React 应用程序时在哪里存储 WebRTC 流

NodeJS Rest API - 调用外部 API 的正确位置在哪里