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

时间:2016-02-18 22:39:44

标签: 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 (Exception ex)
    {
    }

ProcessItem方法做了一些事情,但始终使用以下方法调用API: await SendRequestAsync(..blah)。看起来像:

private async Task<Response> SendRequestAsync(HttpRequestMessage request, CancellationToken token)
{    
    token.ThrowIfCancellationRequested();
    var response = await HttpClient
        .SendAsync(request: request, cancellationToken: token).ConfigureAwait(continueOnCapturedContext: false);

    token.ThrowIfCancellationRequested();
    return await Response.BuildResponse(response);
}

最初代码工作正常,但是当我开始使用Task.WhenAll时,我开始从API获得“超出速率限制”消息。如何限制请求的速率?

值得注意的是,ProcessItem可以根据项目进行1-4次API调用。

3 个答案:

答案 0 :(得分:5)

  

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

<强>更新

  

我的ProcessItems方法根据项目进行1-4次API调用。

在这种情况下,批处理不是一个合适的解决方案。您需要将异步方法限制为某个数字,这意味着SemaphoreSlim。棘手的部分是你希望随着时间的推移允许更多的电话

我还没有尝试过这段代码,但我想要的一般想法就是有一个周期性函数,可以将信号量释放到 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;

答案 1 :(得分:1)

答案类似于this one

不使用任务列表和WhenAll,而是使用Parallel.ForEach并使用ParallelOptions将并发任务数限制为10,并确保每个任务至少需要1秒:

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

或者:

Parallel.ForEach(
    searches,
    new ParallelOptions { MaxDegreeOfParallelism = 10 },
    async item => {
        await Task.WhenAll(
                Task.Delay(1000),
                Task.Run(() => { ProcessItems(item); })
            );
    }
);

答案 2 :(得分:1)

更新的答案

  

我的ProcessItems方法根据项目进行1-4次API调用。因此,批量大小为10时,我仍然超过了速率限制。

您需要在SendRequestAsync中实现滚动窗口。包含每个请求的时间戳的队列是合适的数据结构。您使用超过10秒的时间戳对条目进行出列。碰巧的是,有an implementation作为SO上类似问题的答案。

原始回答

可能对其他人有用

处理此问题的一种简单方法是以10个为一组批量处理请求,同时运行这些请求,然后等待总共10秒(如果尚未执行)。如果一批请求可以在10秒内完成,这将使您处于速率限制,但如果请求批次需要更长时间,则不会达到最佳。看看MoreLinq中的.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
    }
}