Search code examples
c#windows-runtimeasync-awaitwinrt-async

How to use Task.Delay to control time spans between calls to a web service


When a user takes a certain action a call (get) to a web service is made. The service doesn't allow for calls more frequent than once per second. I was thinking I could use Task.Delay to control this so that subsequent calls are made at least one second apart in time but it doesn't seem to work as expected. The pseudo-code looks like this:

public async void OnUserAction()
{
    var timeUntilPreviousCallWillBeOrWasMade = 1000ms - (Now - previousWaitStartTime);
    var timeToWaitBeforeThisCallShouldBeMade = Max(0, timeUntilPreviousCallWillBeOrWasMade + 1000ms);
    previousWaitStartTime = Now;
    await Task.Delay(timeToWaitBeforeThisCallShouldBeMade);
    MakeCallToWebService();
}

Calls and contiunations are done on same thread (as reported by Environment.CurrentManagedThreadId). The problem is that if there's a quick succession of invocations of this method the passed time between calls to the web service is less than 1s. I've either done a silly mistake or I don't fully understand Task.Delay (or both of the above). Any hints?


Solution

  • It would appear that your problem happens when a request is queued up when another one is already waiting. Here's my take on your pseudo code:

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace ThrottledAsync
    {
        class Program
        {
            static void Main(string[] args)
            {
                // Queue up multiple user actions
                // within a short interval.
                for (var i = 0; i < 10; i++)
                {
                    OnUserAction();
                    Thread.Sleep(10);
                }
    
                Console.ReadLine();
            }
    
            private static int UserActionID;
            private static DateTime previousWaitStartTime;
    
            public static async void OnUserAction()
            {
                // Keep track of the operation ID.
                var userActionID = Interlocked.Increment(ref UserActionID);
    
                // Pseudo-code implementation.
                var timeUntilPreviousCallWillBeOrWasMade = 1000 - (int)(DateTime.Now.Subtract(previousWaitStartTime).TotalMilliseconds);
    
                Console.WriteLine(
                    "{0:HH:mm:ss.ffff} - User action {1}: timeUntilPreviousCallWillBeOrWasMade = {2}.",
                    DateTime.Now, userActionID, timeUntilPreviousCallWillBeOrWasMade);
    
                var timeToWaitBeforeThisCallShouldBeMade = Math.Max(0, timeUntilPreviousCallWillBeOrWasMade + 1000);
    
                Console.WriteLine(
                    "{0:HH:mm:ss.ffff} - User action {1}: timeToWaitBeforeThisCallShouldBeMade = {2}.",
                    DateTime.Now, userActionID, timeToWaitBeforeThisCallShouldBeMade);
    
                previousWaitStartTime = DateTime.Now;
    
                await Task.Delay(timeToWaitBeforeThisCallShouldBeMade);
                await MakeCallToWebService(userActionID);
            }
    
            private static async Task MakeCallToWebService(int userActionID)
            {
                // Simulate network delay.
                await Task.Delay(new Random().Next(5, 10));
    
                Console.WriteLine("{0:HH:mm:ss.ffff} - User action {1}: web service call.", DateTime.Now, userActionID);
            }
        }
    }
    

    And the output:

    19:10:11.1366 - User action 1: timeUntilPreviousCallWillBeOrWasMade = -2147482648.
    19:10:11.1416 - User action 1: timeToWaitBeforeThisCallShouldBeMade = 0.
    19:10:11.1536 - User action 2: timeUntilPreviousCallWillBeOrWasMade = 988.
    19:10:11.1536 - User action 2: timeToWaitBeforeThisCallShouldBeMade = 1988.
    19:10:11.1586 - User action 1: web service call.
    19:10:11.1646 - User action 3: timeUntilPreviousCallWillBeOrWasMade = 990.
    19:10:11.1646 - User action 3: timeToWaitBeforeThisCallShouldBeMade = 1990.
    19:10:11.1756 - User action 4: timeUntilPreviousCallWillBeOrWasMade = 990.
    19:10:11.1756 - User action 4: timeToWaitBeforeThisCallShouldBeMade = 1990.
    19:10:11.1866 - User action 5: timeUntilPreviousCallWillBeOrWasMade = 990.
    19:10:11.1866 - User action 5: timeToWaitBeforeThisCallShouldBeMade = 1990.
    19:10:11.1976 - User action 6: timeUntilPreviousCallWillBeOrWasMade = 990.
    19:10:11.1986 - User action 6: timeToWaitBeforeThisCallShouldBeMade = 1990.
    19:10:11.2086 - User action 7: timeUntilPreviousCallWillBeOrWasMade = 990.
    19:10:11.2086 - User action 7: timeToWaitBeforeThisCallShouldBeMade = 1990.
    19:10:11.2186 - User action 8: timeUntilPreviousCallWillBeOrWasMade = 990.
    19:10:11.2196 - User action 8: timeToWaitBeforeThisCallShouldBeMade = 1990.
    19:10:11.2296 - User action 9: timeUntilPreviousCallWillBeOrWasMade = 990.
    19:10:11.2296 - User action 9: timeToWaitBeforeThisCallShouldBeMade = 1990.
    19:10:11.2406 - User action 10: timeUntilPreviousCallWillBeOrWasMade = 990.
    19:10:11.2406 - User action 10: timeToWaitBeforeThisCallShouldBeMade = 1990.
    19:10:13.1567 - User action 2: web service call.
    19:10:13.1717 - User action 3: web service call.
    19:10:13.1877 - User action 5: web service call.
    19:10:13.1877 - User action 4: web service call.
    19:10:13.2107 - User action 6: web service call.
    19:10:13.2187 - User action 7: web service call.
    19:10:13.2187 - User action 8: web service call.
    19:10:13.2357 - User action 9: web service call.
    19:10:13.2537 - User action 10: web service call.
    

    You should really be using the right tool for the job. How about a SemaphoreSlim?

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace ThrottledAsync
    {
        class Program
        {
            static void Main(string[] args)
            {
                // Queue up simultaneous calls.
                MakeThrottledCall();
                MakeThrottledCall();
                MakeThrottledCall();
                MakeThrottledCall();
    
                Console.ReadLine();
            }
    
            // Used to throttle our web service calls.
            // Max degree of parallelism: 1.
            private static readonly SemaphoreSlim WebServiceMutex = new SemaphoreSlim(1, 1);
    
            private static async void MakeThrottledCall()
            {
                // Wait for the previous call
                // (and delay task) to complete.
                await WebServiceMutex.WaitAsync();
    
                try
                {
                    await MakeCallToWebService();
    
                    // Report the completion of your web service call if necessary.
    
                    // Delay for a bit before releasing the semaphore.
                    await Task.Delay(1000);
                }
                finally
                {
                    // Allow the next web service call to go through.
                    WebServiceMutex.Release();
                }
            }
    
            private static async Task MakeCallToWebService()
            {
                // Simulate network delay.
                await Task.Delay(new Random().Next(5, 10));
    
                Console.WriteLine("WebServiceCall: {0:HH:mm:ss.ffff}", DateTime.Now);
            }
        }
    }
    

    EDIT: MakeThrottledCall no longer returns Task as per svick's comment.