Queue of async tasks with throttling which supports muti-threading

前端 未结 5 693
傲寒
傲寒 2020-11-29 04:30

I need to implement a library to request vk.com API. The problem is that API supports only 3 requests per second. I would like to have API asynchronous.

5条回答
  •  日久生厌
    2020-11-29 04:47

    So we'll start out with a solution to a simpler problem, that of creating a queue that process up to N tasks concurrently, rather than throttling to N tasks started per second, and build on that:

    public class TaskQueue
    {
        private SemaphoreSlim semaphore;
        public TaskQueue()
        {
            semaphore = new SemaphoreSlim(1);
        }
        public TaskQueue(int concurrentRequests)
        {
            semaphore = new SemaphoreSlim(concurrentRequests);
        }
    
        public async Task Enqueue(Func> taskGenerator)
        {
            await semaphore.WaitAsync();
            try
            {
                return await taskGenerator();
            }
            finally
            {
                semaphore.Release();
            }
        }
        public async Task Enqueue(Func taskGenerator)
        {
            await semaphore.WaitAsync();
            try
            {
                await taskGenerator();
            }
            finally
            {
                semaphore.Release();
            }
        }
    }
    

    We'll also use the following helper methods to match the result of a TaskCompletionSource to a `Task:

    public static void Match(this TaskCompletionSource tcs, Task task)
    {
        task.ContinueWith(t =>
        {
            switch (t.Status)
            {
                case TaskStatus.Canceled:
                    tcs.SetCanceled();
                    break;
                case TaskStatus.Faulted:
                    tcs.SetException(t.Exception.InnerExceptions);
                    break;
                case TaskStatus.RanToCompletion:
                    tcs.SetResult(t.Result);
                    break;
            }
    
        });
    }
    
    public static void Match(this TaskCompletionSource tcs, Task task)
    {
        Match(tcs, task.ContinueWith(t => default(T)));
    }
    

    Now for our actual solution what we can do is each time we need to perform a throttled operation we create a TaskCompletionSource, and then go into our TaskQueue and add an item that starts the task, matches the TCS to its result, doesn't await it, and then delays the task queue for 1 second. The task queue will then not allow a task to start until there are no longer N tasks started in the past second, while the result of the operation itself is the same as the create Task:

    public class Throttler
    {
        private TaskQueue queue;
        public Throttler(int requestsPerSecond)
        {
            queue = new TaskQueue(requestsPerSecond);
        }
        public Task Enqueue(Func> taskGenerator)
        {
            TaskCompletionSource tcs = new TaskCompletionSource();
            var unused = queue.Enqueue(() =>
            {
                tcs.Match(taskGenerator());
                return Task.Delay(TimeSpan.FromSeconds(1));
            });
            return tcs.Task;
        }
        public Task Enqueue(Func taskGenerator)
        {
            TaskCompletionSource tcs = new TaskCompletionSource();
            var unused = queue.Enqueue(() =>
            {
                tcs.Match(taskGenerator());
                return Task.Delay(TimeSpan.FromSeconds(1));
            });
            return tcs.Task;
        }
    }
    

提交回复
热议问题