调用 API 时在哪里使用并发

Where to use concurrency when calling an API

在一个 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 之前)

既然我看到它已发布,我想我也不能只删除变量 response,只是执行 await 而不将其设置为任何变量

谢谢

这个我自己没有验证过,这只是我搜索的结果。因此,您不必为开始的每个任务都创建一个新的客户端。你可以做你最方便的事情。

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

不要忘记 await WhenAll 任务并制作方法 async Task。 (如果你不明白为什么这是必要的,你需要做一些关于 await 的研究。)

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

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);   

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

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

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

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

首先,您应该在使用 Task.WhenAll 时将您的方法标记为 async Task,return 是一个可等待对象,您需要对其进行异步等待。接下来,您可以简单地 return 来自 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 link),因此我看不出有什么理由每次都在您的 lambda 中创建一个新实例。请注意,如果您多次使用 DeployViewAsync,也许您会希望保留 HttpClient 而不是每次都分配一个,并在您不再需要它的服务时处理它。