Queuing asynchronous task in C#

前端 未结 4 1040
慢半拍i
慢半拍i 2021-01-28 14:46

I have few methods that report some data to Data base. We want to invoke all calls to Data service asynchronously. These calls to data service are all over and so we want to mak

4条回答
  •  礼貌的吻别
    2021-01-28 15:07

    Initially, i was using async await on each of these methods and each of the calls were executed asynchronously but we found out if they are out of sequence then there are room for errors.

    So, i thought we should queue all these asynchronous tasks and send them in a separate thread but i want to know what options we have? I came across 'SemaphoreSlim' .

    SemaphoreSlim does restrict asynchronous code to running one at a time, and is a valid form of mutual exclusion. However, since "out of sequence" calls can cause errors, then SemaphoreSlim is not an appropriate solution since it does not guarantee FIFO.

    In a more general sense, no synchronization primitive guarantees FIFO because that can cause problems due to side effects like lock convoys. On the other hand, it is natural for data structures to be strictly FIFO.

    So, you'll need to use your own FIFO queue, rather than having an implicit execution queue. Channels is a nice, performant, async-compatible queue, but since you're on an older version of C#/.NET, BlockingCollection would work:

    public sealed class ExecutionQueue
    {
      private readonly BlockingCollection> _queue = new BlockingCollection>();
    
      public ExecutionQueue() => Complete = Task.Run(() => ProcessQueueAsync());
    
      public Task Completion { get; }
    
      public void Complete() => _queue.CompleteAdding();
    
      private async Task ProcessQueueAsync()
      {
        foreach (var value in _queue.GetConsumingEnumerable())
          await value();
      }
    }
    

    The only tricky part with this setup is how to queue work. From the perspective of the code queueing the work, they want to know when the lambda is executed, not when the lambda is queued. From the perspective of the queue method (which I'm calling Run), the method needs to complete its returned task only after the lambda is executed. So, you can write the queue method something like this:

    public Task Run(Func lambda)
    {
      var tcs = new TaskCompletionSource();
      _queue.Add(async () =>
      {
        // Execute the lambda and propagate the results to the Task returned from Run
        try
        {
          await lambda();
          tcs.TrySetResult(null);
        }
        catch (OperationCanceledException ex)
        {
          tcs.TrySetCanceled(ex.CancellationToken);
        }
        catch (Exception ex)
        {
          tcs.TrySetException(ex);
        }
      });
      return tcs.Task;
    }
    
    
    

    This queueing method isn't as perfect as it could be. If a task completes with more than one exception (this is normal for parallel code), only the first one is retained (this is normal for async code). There's also an edge case around OperationCanceledException handling. But this code is good enough for most cases.

    Now you can use it like this:

    public static ExecutionQueue _queue = new ExecutionQueue();
    
    public async Task SendModuleDataToDSAsync(Module parameters)
    {
      var tasks1 = new List();
      var tasks2 = new List();
    
      foreach (var setting in Module.param)
      {
        Task job1 = _queue.Run(() => SaveModule(setting));
        tasks1.Add(job1);
        Task job2 = _queue.Run(() => SaveModule(GetAdvancedData(setting)));
        tasks2.Add(job2);
      }
    
      await Task.WhenAll(tasks1);
      await Task.WhenAll(tasks2);
    }
    

    提交回复
    热议问题