【问题标题】:How can I make 50 requests per second without having to wait an additional second as per logic given below?根据下面给出的逻辑,我怎样才能每秒发出 50 个请求而不必再等待一秒钟?
【发布时间】:2021-10-29 23:49:12
【问题描述】:

我有一个 Windows 窗体应用程序,单击按钮时,我想循环一个整数列表并以每秒只能发出 50 个并行请求的方式进行 http post 调用。这是因为目标 http 端点仅支持每秒 50 个请求。为了实现这一点,这是我的逻辑:

我已将 SemaphoreSlim 节流器声明为全局变量:

var throttler = new SemaphoreSlim(50);

在按钮单击事件中,我正在遍历项目列表并进行 http 调用:

        List<Task> lstTasks = new List<Task>();
        foreach (var item in lstItems)
        {
            lstTasks.Add(CallAsyncMtd(item));
        }

进行http调用的方法是:

private async Task CallAsyncMtd(int item, IProgress<int> progress)
{
    await throttler.WaitAsync(); // here I am doing an async wait for the semaphore so that not more than 5 threads can run at the same time

    try
    {
        await Task.Delay(1000).ConfigureAwait(false); //simulate an api call, configure await is false so the remaining code will run on separate thread            
    }
    catch (Exception e)
    {
        //do some exception handling
    }

    //saveResponseToMySQL(response) //save the http response to mysql database synchronously
    progress.Report(1); //report progress
    
    await Task.Delay(1000 * 1); //wait for 1 second
    throttler.Release(); //release semaphore
}

发出请求后,我正在等待 1 秒(在单独的线程中)。有没有更好的方法可以让我每秒发出 50 个请求,而不必按照上述逻辑再等待一秒钟?

【问题讨论】:

  • 你还等什么呢?
  • 信号量允许50个线程同时运行。我正在等待,以便在一秒钟内发出不超过 50 个请求。例如 - 如果我删除了 1 秒的等待,那么在一秒钟内可能会出现这样的情况,即发出 50 个请求,并且说其中 1 个已完成并发出第 51 个请求。我想防止这种情况发生。
  • 定义“每秒”。你的意思是'一旦你开始一个请求,你就不能开始第 51 个请求,直到 1000 毫秒过去'(即时间窗口是基于第一个请求,还是基于时钟的第二个窗口)? 具体是什么速率限制规则?只有一个进程启动这些请求吗?您发起的请求总数是多少?您是否考虑过将多个数字传递给单个 API 调用(而不是发起多个 API 调用)?
  • 需求文档说我们每秒最多只能发出 50 个请求。所以在一秒钟内不能有超过 50 个请求。如果我们确实提出超过 50 个,那么我们必须为所有请求支付费用。所以我想避免这笔费用。
  • Interlocked.Increment 在每个请求上。每 45 个请求将当前时间与之前的 45 个请求进行比较。如果需要,添加睡眠。我认为如果你在await 之前这样做,那么它应该做你想做的事。

标签: c# winforms asynchronous async-await


【解决方案1】:

在函数开始时使用秒表,而不是在结束时检查已经过去了多少时间,并且只检查Delay 所需的时间。

类似这样的:

private async Task CallAsyncMtd(int item, IProgress<int> progress)
{
    await throttler.WaitAsync(); // here I am doing an async wait for the semaphore so that not more than 5 threads can run at the same time

    var watch = new StopWatch();
    watch.Start();
    try
    {
        await ApiCall(); //simulate an api call, configure await is false so the remaining code will run on separate thread            
    }
    catch (Exception e)
    {
        //do some exception handling
    }

    //saveResponseToMySQL(response) //save the http response to mysql database synchronously
    progress.Report(1); //report progress
    watch.Stop();
    var timeToWait = Math.Max(0, 1000 - watch.Elapsed.Milliseconds);
    await Task.Delay(timeToWait); //wait for necessary amount of time
    throttler.Release(); //release semaphore
}

【讨论】:

  • 看起来很有趣。这是否适用于configure await False,其中http api调用之后的代码在单独的线程上运行?我的意思是 - 秒表可以跨线程工作吗?
  • 我看不出有什么理由。它是async 函数内的局部变量。您永远不会同时接触来自 2 个不同线程的同一个 StopWatch
【解决方案2】:

您无法将Task.delay 添加到CallAsyncMtd() 方法中,请尝试:

foreach (var item in lstItems)
{
    await Task.Delay(20);
    lstTasks.Add(CallAsyncMtd(item));
}

【讨论】:

  • 1000 是毫秒,因为 delay(1000) 是 1 秒,所以 /50 是 20,任务是 20 毫秒
  • 好吧,你是说不需要 SemaphoreSlim/thottler?
【解决方案3】:

您可以链接延迟并释放信号量,但不能等待它。这会阻塞 Semaphore 额外的一秒钟,但会立即允许处理结果。

// Wait 1 second befor releasing the semaphore but do not block this request.
Task.Delay(1000 *1 ).ContinueWith(_ => throttler.Release());

如果这会导致很大的延迟,您可以将其与此答案https://stackoverflow.com/a/68998020/9271844 结合使用以减少延迟。

【讨论】:

  • 注意ContinueWith在同步上下文方面有一些陷阱,所以如果你想避免它,可以重构调用方法以启动Delays的函数并释放信号量,但您可以同时处理结果,而不是立即awaiting。
【解决方案4】:

我会为此使用System.Threading.Tasks.Dataflow。使用 ActionBlock,您可以指定任务的并行度,如果有 50 个任务在执行一次调用后休眠,则每秒只有 50 个调用:

//define the ActionBlock to call CallAsyncMtd for each item
var block = new ActionBlock<int>(
    async item => { 
        await CallAsyncMtd(item, progress);
    },
    // Specify a maximum degree of parallelism.
    new ExecutionDataflowBlockOptions
    {
        MaxDegreeOfParallelism = 50
    });


//Define a BatchBlock and link it to a ActionBlock to generate Blocks of 50 items
var linkOptions = new DataflowLinkOptions { PropagateCompletion = true };


 var oneSecondBatchBlock = new BatchBlock<int>(buffSize);
 var oneSecondStatsFeedBlock = new ActionBlock<int[]>(
     async (int[] messages) => 
            {
                var watch = new Stopwatch();
                watch.Start();
                //Add to ActionBlock doing the call
                foreach (int item in messages) block.Post(item);


                watch.Stop();
                var timeToWait = Math.Max(0, 1000 - watch.Elapsed.Milliseconds);
                await Task.Delay(timeToWait);
                   
               });

 //Link Blocks
 oneSecondBatchBlock.LinkTo(oneSecondStatsFeedBlock, linkOptions);

       

        

// Add items to block
foreach (var item in lstItems)
{
     oneSecondBatchBlock.SendAsync(i).Wait();//Add Work
}
    
//wait for finish
oneSecondBatchBlock.Complete();            
await oneSecondBatchBlock.Completion;
block.Complete();
await block.Completion;

-- 编辑--

使用 BatchBlock 将工作分成 50 个部分更有意义。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-12-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-08-20
    • 1970-01-01
    相关资源
    最近更新 更多