C# 中是否有一种内置的方法来限制函数调用的次数?

8

我有一个执行HTTP请求的单一函数。 我在编写的简单程序中频繁地使用这个函数。

然而,我需要限制这些HTTP请求,以避免超过速率限制。 在JavaScript世界中,有一个非常方便的第三方库,它提供了一个throttle函数,返回一个新函数调用您自己的函数,但会排队处理调用,使它们每分钟或每小时只发生X次。

那么,在C#中有没有内置的方法来实现这一点呢? 如果没有,我该如何完成这个功能呢?


你可以轻松地使用 Task.Delay(...) 或手动计算自上次调用以来的时间差,如果大于你的阈值,则执行操作。 - SharpShade
@SharpShade在调用频率接近或超过计时器分辨率(约为15毫秒)时无法正常工作。此外,它也不支持并发调用。 - usr
我认为他希望限制请求方法的频率,因此假定其频率超出计时器分辨率。特别是您可能至少有 ~5+ 毫秒(取决于您的服务器)来处理请求。 - SharpShade
在微服务架构中广泛使用的一种最佳实践是Polly。更通用的解决方案是使用队列。如果您担心线程问题,请考虑使用ConcurrentQueue<T>。实现很简单,但您需要自己编写十几行代码将其打包成可重用组件以满足您的需求。 - Chris Schaller
6个回答

6

5

没有内置的方法。您需要找到一个库或自己实现。


2

正如其他答案所说,您需要自己实现这个功能。幸运的是,这很容易。个人建议创建一个Queue<ObjectWithYourFunction'sArguments>。然后,您可以创建一个ThrottledFunction将其排队,如果需要,启动一个后台任务等待适当的时间。

以下是未经测试的示例代码:

class ThrottleMyFunction
{
    private class Arguments
    {
        public int coolInt;
        public double whatever;
        public string stringyThing;
    }

    private ConcurrentQueue<Arguments> _argQueue = new ConcurrentQueue<Arguments>();
    private Task _loop;

    //If you want to do "X times per minute", replace this argument with an int and use TimeSpan.FromMinutes(1/waitBetweenCalls)
    public void ThrottleMyFunction(TimeSpan waitBetweenCalls)
    {
        _loop = Task.Factory.StartNew(() =>
        {
            Arguments args;
            while (true)
            {
                if (_argQueue.TryDequeue(out args))
                    FunctionIWantToThrottle(args.coolInt, args.whatever, args.stringyThing);
            }

            Thread.Sleep(waitBetweenCalls);

        });
    }

    public void ThrottledFunction(int coolerInt, double whatevs, string stringy)
    {
        _argQueue.Enqueue(new Arguments() { coolInt = coolerInt, whatever = whatevs, stringyThing = stringy });
    }
}

2
我会使用ConcurrentQueue。这是一个Web应用程序,因此您将在多个线程上排队项目,并可能在另一个线程上出列。 - Scott Hannen
很好的建议,@ScottHannen。我也在考虑那样做。 - Grace

1
我写了这个,但由于其本质的特殊性质,测试它可能会有些复杂(也就是说,它没有经过测试)。
假设您有一个要调用的>,因此该操作作为参数传递给构造函数。然后要执行,您需要调用。
它将排队您的项目并开始处理队列(除非队列已在处理中)。每次执行都会增加一个计数器,当计数器达到每个间隔的最大执行次数时,执行停止。
还有一个指定间隔的计时器。当计时器到期时,执行次数将重置,并且将处理队列(除非它已在处理中)。这样等待间隔结束的项目将被处理。
这是对您要求的非常直接的解释。它不会在间隔内均匀地分散调用,也不会在它们之间等待一段时间。这意味着如果您只能在一秒钟内进行3次给定的调用,它将立即进行前3次调用,然后等待第二次调用之前的时间再进行3次调用。目标是在不等待的情况下使用任何容量,然后等待更多容量可用并在不等待的情况下使用它。
using System;
using System.Collections.Concurrent;
using System.Threading;
using Timer = System.Timers.Timer;

namespace Throttler
{
    public abstract class ExecutionThrottler<TParameters> : IDisposable
    {
        private readonly Action<TParameters> _action;
        private readonly int _executionsPerInterval;
        private readonly ConcurrentQueue<TParameters> _queue = new ConcurrentQueue<TParameters>();
        private bool _processingQueue;
        private readonly object _processingQueueLock = new object();
        private int _executionsSinceIntervalStart;
        private readonly Timer _timer;
        bool _disposed;

        protected ExecutionThrottler(Action<TParameters> action, TimeSpan interval, int executionsPerInterval)
        {
            _action = action;
            _executionsPerInterval = executionsPerInterval;
            _timer = new Timer(interval.TotalMilliseconds);
            _timer.AutoReset = true;
            _timer.Start();
            _timer.Elapsed += OnIntervalEnd;
        }

        public void Enqueue(TParameters parameters)
        {
            _queue.Enqueue(parameters);
        }

        private void TryProcessQueue()
        {
            if (_processingQueue) return;
            lock (_processingQueueLock)
            {
                if (_processingQueue) return;
                _processingQueue = true;
                try
                {
                    ProcessQueue();
                }
                finally
                {
                    _processingQueue = false;
                }
            }
        }

        private void ProcessQueue()
        {
            TParameters dequeuedParameters;
            while ((_executionsSinceIntervalStart < _executionsPerInterval) && _queue.TryDequeue(out dequeuedParameters))
            {
                Interlocked.Increment(ref _executionsSinceIntervalStart);
                _action.Invoke(dequeuedParameters);
            }
        }


        private void OnIntervalEnd(object sender, System.Timers.ElapsedEventArgs e)
        {
            _executionsSinceIntervalStart = 0;
            TryProcessQueue();
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        ~ExecutionThrottler()
        {
            Dispose(false);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (_disposed)
                return;

            if (disposing)
            {
                _timer.Dispose();
            }
            _disposed = true;
        }
    }
}

更新:编辑以删除不必要的使用Interlocked以确保原子读/写。唯一需要它的操作是增加执行操作的计数。

更新为:我过度使用了 Interlocked_executionsSinceIntervalStart 可以是一个 int,除了增加之外的所有其他操作都已经是原子操作了。 - Scott Hannen

0

-1

不,.Net Framework 中没有内置的限流功能。您需要构建自己的限流功能或查找现有的库。

我认为在框架内最接近的方法是对像 Parallel.Foreach 这样的调用设置线程数限制。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接