C# 对HttpClient请求进行速率限制的简单方法
我使用System.Net.Http中的HTTPClient对API发出请求。API限制为每秒10个请求 我的代码大致如下:C# 对HttpClient请求进行速率限制的简单方法,c#,async-await,C#,Async Await,我使用System.Net.Http中的HTTPClient对API发出请求。API限制为每秒10个请求 我的代码大致如下: List<Task> tasks = new List<Task>(); items..Select(i => tasks.Add(ProcessItem(i)); try { await Task.WhenAll(taskList.ToArray()); } catch (E
List<Task> tasks = new List<Task>();
items..Select(i => tasks.Add(ProcessItem(i));
try
{
await Task.WhenAll(taskList.ToArray());
}
catch (Exception ex)
{
}
最初代码运行良好,但当我开始使用Task.whalll时,我开始从API收到“超出速率限制”消息。我如何限制提出请求的速率
值得注意的是,ProcessItem可以根据项目进行1-4次API调用 答案类似于
使用和使用将并发任务的数量限制为10个,并确保每个任务至少需要1秒,而不是使用任务列表和whalll
:
Parallel.ForEach(
items,
new ParallelOptions { MaxDegreeOfParallelism = 10 },
async item => {
ProcessItems(item);
await Task.Delay(1000);
}
);
或者,如果您希望确保每个项目花费尽可能接近1秒的时间:
Parallel.ForEach(
searches,
new ParallelOptions { MaxDegreeOfParallelism = 10 },
async item => {
var watch = new Stopwatch();
watch.Start();
ProcessItems(item);
watch.Stop();
if (watch.ElapsedMilliseconds < 1000) await Task.Delay((int)(1000 - watch.ElapsedMilliseconds));
}
);
更新的答案 我的ProcessItems方法根据项目进行1-4次API调用。因此,批量大小为10时,我仍然超出了速率限制 您需要在SendRequestAsync中实现一个滚动窗口。包含每个请求的时间戳的队列是合适的数据结构。您可以将时间戳早于10秒的条目出列。碰巧的是,有一个类似问题的答案 原始答案 可能对其他人仍然有用 处理这一问题的一种简单方法是,将请求分成10组进行批处理,并发运行这些请求,然后等待总共10秒(如果还没有的话)。如果一批请求可以在10秒钟内完成,这将使您以正确的速率限制进入,但如果一批请求需要更长的时间,这将不是最佳的。请查看中的.Batch()扩展方法。代码大致如下所示
foreach (var taskList in tasks.Batch(10))
{
Stopwatch sw = Stopwatch.StartNew(); // From System.Diagnostics
await Task.WhenAll(taskList.ToArray());
if (sw.Elapsed.TotalSeconds < 10.0)
{
// Calculate how long you still have to wait and sleep that long
// You might want to wait 10.5 or 11 seconds just in case the rate
// limiting on the other side isn't perfectly implemented
}
}
foreach(任务中的var任务列表。批处理(10))
{
Stopwatch sw=Stopwatch.StartNew();//来自System.Diagnostics
wait Task.WhenAll(taskList.ToArray());
如果(sw.Essed.TotalSeconds<10.0)
{
//计算一下你还要等多久,还要睡多久
//您可能需要等待10.5秒或11秒,以防速率下降
//另一方面的限制并没有完全实现
}
}
API限制为每秒10个请求
然后让您的代码执行一批10个请求,确保它们至少需要一秒钟:
Items[] items = ...;
int index = 0;
while (index < items.Length)
{
var timer = Task.Delay(TimeSpan.FromSeconds(1.2)); // ".2" to make sure
var tasks = items.Skip(index).Take(10).Select(i => ProcessItemsAsync(i));
var tasksAndTimer = tasks.Concat(new[] { timer });
await Task.WhenAll(tasksAndTimer);
index += 10;
}
用法:
// Start the periodic task, with a signal that we can use to stop it.
var stop = new TaskCompletionSource<object>();
var periodicTask = PeriodicallyReleaseAsync(stop.Task);
// Wait for all item processing.
await Task.WhenAll(taskList);
// Stop the periodic task.
stop.SetResult(null);
await periodicTask;
//启动周期性任务,并发出一个我们可以用来停止它的信号。
var stop=new TaskCompletionSource();
var periodicTask=periodicallyreleaseaseanc(stop.Task);
//等待所有项目处理。
等待任务。WhenAll(任务列表);
//停止定期任务。
stop.SetResult(空);
等待提问;
任何时候项目中有多少请求?您具体在哪里形成任务列表?有18000个项目,很多。是否可以在响应中发现您已达到限制,然后等待所需时间再重试。如果请求在不到一秒钟内完成,这对速率限制没有帮助,因为限制是基于每秒请求而不是并发请求。对,我错过了。我已经编辑了我的答案,为每个项目添加了一个延迟。虽然这会起作用,但它并不是最优的,因为每秒最多会有10个请求(如果ProcessItems()不花时间,您就会得到)。这就像21点一样。他想在不经过检查的情况下,以接近10个请求/秒的速度完成。我添加了一些选项。我确实尝试过这个想法,但没有成功。我的ProcessItems方法根据项目进行1-4次API调用。因此,批量大小为10时,我仍然超出了速率限制。如果批量较小,比如说5个,处理18000个项目需要相当长的时间。类似于上面的答案。我的ProcessItems方法根据项目进行1-4次API调用。在我看来,这促使我实现尽可能接近api调用的速率限制。您持有的任何请求都将耗尽http连接,最终可能会使您的服务器陷入瓶颈。如果可以,当请求超出阈值范围时,通过拒绝响应状态代码为429的请求来限制。是否有办法将其调整为仅在HttpClient.GetAsync
s之间等待50毫秒?我还向他们发送了一个与原始问题类似的LINQ Select。@Hershizer33:使用async
可以做到这一点,但使用Rx
更容易表达@johnny5:我相信我之所以这么说,是因为如果对项目进行批处理,那么每秒10个请求的上限可能无法满足。但是,如果所有请求都已排队并被限制,那么代码始终可以每秒发送10次,直到队列耗尽为止。节流通过调用WaitAsync
完成。
Items[] items = ...;
int index = 0;
while (index < items.Length)
{
var timer = Task.Delay(TimeSpan.FromSeconds(1.2)); // ".2" to make sure
var tasks = items.Skip(index).Take(10).Select(i => ProcessItemsAsync(i));
var tasksAndTimer = tasks.Concat(new[] { timer });
await Task.WhenAll(tasksAndTimer);
index += 10;
}
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(10);
private async Task<Response> ThrottledSendRequestAsync(HttpRequestMessage request, CancellationToken token)
{
await _semaphore.WaitAsync(token);
return await SendRequestAsync(request, token);
}
private async Task PeriodicallyReleaseAsync(Task stop)
{
while (true)
{
var timer = Task.Delay(TimeSpan.FromSeconds(1.2));
if (await Task.WhenAny(timer, stop) == stop)
return;
// Release the semaphore at most 10 times.
for (int i = 0; i != 10; ++i)
{
try
{
_semaphore.Release();
}
catch (SemaphoreFullException)
{
break;
}
}
}
}
// Start the periodic task, with a signal that we can use to stop it.
var stop = new TaskCompletionSource<object>();
var periodicTask = PeriodicallyReleaseAsync(stop.Task);
// Wait for all item processing.
await Task.WhenAll(taskList);
// Stop the periodic task.
stop.SetResult(null);
await periodicTask;