C# 对HttpClient请求进行速率限制的简单方法

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

我使用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 (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;