C#并发多个并行HttpClient调用超时

C# Concurrent multiple in Parallel HttpClient calls with timeout

我有一个 API 必须并行调用 4 HttpClients 支持每秒 500 个用户的并发(他们都调用 API同时)

即使并非所有 HttpClients 调用都有 return 值,也必须有严格的超时让 API 到 return 结果。
端点是外部第三方 API,我无法控制它们或不知道代码。
我对此事进行了广泛的研究,但即使许多解决方案都有效,我也需要尽可能少消耗 CPU 的解决方案,因为我的服务器预算很低。

到目前为止我想到了这个:

var conn0 = new HttpClient
{
    Timeout = TimeSpan.FromMilliseconds(1000),
    BaseAddress = new Uri("http://endpoint")
};
var conn1 = new HttpClient
{
    Timeout = TimeSpan.FromMilliseconds(1000),
    BaseAddress = new Uri("http://endpoint")
};
var conn2 = new HttpClient
{
    Timeout = TimeSpan.FromMilliseconds(1000),
    BaseAddress = new Uri("http://endpoint")
};
var conn3 = new HttpClient
{
    Timeout = TimeSpan.FromMilliseconds(1000),
    BaseAddress = new Uri("http://endpoint")
};

var list = new List<HttpClient>() { conn0, conn1, conn2, conn3 };
var timeout = TimeSpan.FromMilliseconds(1000);
var allTasks = new List<Task<Task>>();

//the async DoCall method just call the HttpClient endpoint and return a MyResponse object
foreach (var call in list)
{
    allTasks.Add(Task.WhenAny(DoCall(call), Task.Delay(timeout)));
}
var completedTasks = await Task.WhenAll(allTasks);
var allResults = completedTasks.OfType<Task<MyResponse>>().Select(task => task.Result).ToList();
return allResults;

我使用 WhenAny 和两个任务,一个用于调用,一个用于 timeout.If 调用任务迟到,另一个 return 无论如何。

现在,这段代码运行完美,一切都是异步的,但我想知道是否有更好的方法来实现这一目标。
对这个 API 的一次调用会创建大量线程,并且有 500 个并发用户时,它需要平均 8(八)台 D3_V2 Azure 4 核机器,导致疯狂的开支,超时时间越高, CPU 使用率更高。

有没有更好的方法来做到这一点而不使用那么多 CPU 资源(也许 Parallel Linq 是比这更好的选择)?

仅 HttpClient 超时是否足以停止呼叫,return 如果端点没有及时回复,而不必使用 WhenAny 中的第二个任务?

更新:

您使用这两个任务来超时的方法确实有效,但您可以做更好的事情:use CancellationToken for the task,以及从服务器获取答案:

var cts = new CancellationTokenSource();
// set the timeout equal to the 1 second
cts.CancelAfter(1000);
// provide the token for your request
var response = await client.GetAsync(url, cts.Token);  

之后,您可以简单地过滤 completed 个任务:

var allResults = completedTasks
    .Where(t => t.IsCompleted)
    .Select(task => task.Result).ToList();

这种方法将使您创建的任务数量减少不少于两倍,并且会降低服务器的开销。此外,它将为您提供一种简单的方法来取消部分处理甚至整个处理。如果您的任务彼此完全独立,您可以使用 Parallel.For 来调用 http 客户端,但仍然使用 token for cancelling the operation:

ParallelLoopResult result = Parallel.For(list, call => DoCall(call, cts.Token));
// handle the result of the parallel tasks

或者,使用 PLINQ:

var results = list
    .AsParallel()
    .Select(call => DoCall(call, cts.Token))
    .ToList();