3

Using TPL / Parallel.ForEach is there an out-of-the-box way to limit the number of times a method is called per unit of time (i.e. no more than 50 calls per second). This is different than limiting the number of threads. Perhaps there's some simple hack to make this work?

svick
  • 236,525
  • 50
  • 385
  • 514
Suraj
  • 35,905
  • 47
  • 139
  • 250
  • Could you explain in more detail what exactly do you need and why? – svick May 03 '13 at 14:33
  • I need to call a web API in parallel, but the API limits the number of calls per second. I want to stay under that limit. – Suraj May 03 '13 at 14:40

3 Answers3

3

One solution is to make a thread-safe version of the following https://stackoverflow.com/a/7728872/356790

/// <summary>
/// This class limits the number of requests (method calls, events fired, etc.) that can occur in a given unit of time.
/// </summary>
class RequestLimiter
{

    #region Constructors

    /// <summary>
    /// Initializes an instance of the RequestLimiter class.
    /// </summary>
    /// <param name="maxRequests">The maximum number of requests that can be made in a given unit of time.</param>
    /// <param name="timeSpan">The unit of time that the maximum number of requests is limited to.</param>
    /// <exception cref="ArgumentException">maxRequests &lt;= 0</exception>
    /// <exception cref="ArgumentException">timeSpan.TotalMilliseconds &lt;= 0</exception>
    public RequestLimiter( int maxRequests , TimeSpan timeSpan )
    {
        // check parameters
        if ( maxRequests <= 0 )
        {
            throw new ArgumentException( "maxRequests <= 0" , "maxRequests" );
        }
        if ( timeSpan.TotalMilliseconds <= 0 )
        {
            throw new ArgumentException( "timeSpan.TotalMilliseconds <= 0" , "timeSpan" );
        }

        // initialize instance vars
        _maxRequests = maxRequests;
        _timeSpan = timeSpan;
        _requestTimes = new Queue<DateTime>( maxRequests );

        // sleep for 1/10th timeSpan
        _sleepTimeInMs = Convert.ToInt32( Math.Ceiling( timeSpan.TotalMilliseconds / 10 ) );
    }

    #endregion

    /// <summary>
    /// Waits until an request can be made
    /// </summary>
    public void WaitUntilRequestCanBeMade()
    {
        while ( !TryEnqueueRequest() )
        {
            Thread.Sleep( _sleepTimeInMs );
        }
    }

    #region Private Members

    private readonly Queue<DateTime> _requestTimes;
    private readonly object _requestTimesLock = new object();
    private readonly int _maxRequests;
    private readonly TimeSpan _timeSpan;
    private readonly int _sleepTimeInMs;

    /// <summary>
    /// Remove requests that are older than _timeSpan
    /// </summary>
    private void SynchronizeQueue()
    {
        while ( ( _requestTimes.Count > 0 ) && ( _requestTimes.Peek().Add( _timeSpan ) < DateTime.Now ) )
        {
            _requestTimes.Dequeue();
        }
    }

    /// <summary>
    /// Attempts to enqueue a request.
    /// </summary>
    /// <returns>
    /// Returns true if the request was successfully enqueued.  False if not.
    /// </returns>
    private bool TryEnqueueRequest()
    {
        lock ( _requestTimesLock )
        {
            SynchronizeQueue();
            if ( _requestTimes.Count < _maxRequests )
            {
                _requestTimes.Enqueue( DateTime.Now );
                return true;
            }
            return false;
        }
    }

    #endregion

}
Community
  • 1
  • 1
Suraj
  • 35,905
  • 47
  • 139
  • 250
  • You seem to have answered a different question. The necessity "to limit executions" (["to call a web API in parallel"](http://stackoverflow.com/questions/16360733/in-parallel-call-limit-executions-per-second/16402417#comment23442116_16360733)) doesn't imply such **super-over-complications** as making more executions/requests than the limit and en-queue-ing/delaying its excess into a queue! – Gennady Vanin Геннадий Ванин May 06 '13 at 16:28
  • I have no idea what you're trying to say. I'm trying to limit the number of calls to X/second. This is accomplished by calling WaitUntilRequestCanBeMade() prior to making an API call. – Suraj May 06 '13 at 17:08
  • In order to limit the number of executions (calls or whatever) there is no need to queue/dequeue them. Just do not make more calls per second than the required limit. As simple as that! [In my answer](http://stackoverflow.com/a/16402417/200449) I gave the references to ready code examples of such realizations without any queuing/de-queuing. If the flow of calls is out of your control, then your answer is a valid approach but has nothing to do with your question as it was posted. You post (underspecified) question, answer another one and accept it! – Gennady Vanin Геннадий Ванин May 06 '13 at 18:11
  • I think you are too narrowly focused on your solution to see the merits of mine. "Just do not make more calls per second than the required limit" is a loaded statement. Do you just drop the calls that are in excess of the limit? If not, then you have to queue them somehow. What if the web service has a system-wide limit and you are calling more than one method? The solution I presented works well for all cases and it's efficient. I'm not saying your solution is flawed, I just have no reason to investigate because I have a solution at hand. – Suraj May 06 '13 at 20:18
  • 4
    By the way, I see that you just downvoted a bunch of my posts and I'm pretty sure you don't have the expertise in all of these areas to determine which ones are valid solutions =) It's not the best behavior to bring to this community. Accepting answers doesn't increase reputation points. I often accept my own answers when I think it will help other users. I have specifically not selected your answer because none of the links directly solve for the posted problem, whereas my solution can be copy-pasted. – Suraj May 06 '13 at 20:27
  • 1
    I really like this, is simple to use and means I can create as many different request limit objects depending on my use. I am using this to limit calls to things like facebook, twitter, and other social media. – jimplode Oct 01 '14 at 14:26
0

The ready code samples using Timer:

The code samples/examples using Reactive Extensions (Rx):

Community
  • 1
  • 1
0

This solution enforces a delay between the start of each thread and could be used to fulfill your requirements.

    private SemaphoreSlim CooldownLock = new SemaphoreSlim(1, 1);
    private DateTime lastAction;

    private void WaitForCooldown(TimeSpan delay)
    {
        CooldownLock.Wait();

        var waitTime = delay - (DateTime.Now - lastAction);

        if (waitTime > TimeSpan.Zero)
        {
            Task.Delay(waitTime).Wait();
            lastAction = DateTime.Now;
        }

        lastAction = DateTime.Now;

        CooldownLock.Release();
    }

    public void Execute(Action[] actions, int concurrentThreadLimit, TimeSpan threadDelay)
    {
        if (actions.Any())
        {
            Parallel.ForEach(actions, 
                             new ParallelOptions() { MaxDegreeOfParallelism = concurrentThreadLimit}, 
                            (currentAction) =>
                            {
                                WaitForCooldown(threadDelay);
                                currentAction();
                            });
        }
    }
mcwyrm
  • 1,571
  • 1
  • 15
  • 27