How to use Task.Delay to control time spans betwee

2019-09-15 20:27发布

问题:

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?

回答1:

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.



回答2:

Can you use Thread.Sleep(millisecondsTimeout) at msdn?

I suggest you to use that.