Queuing asynchronous task in C#

前端 未结 4 1039
慢半拍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 14:53

    Building on your comment under Alexeis answer, your approch with the SemaphoreSlim is correct.

    Assumeing that the methods SendInstrumentSettingsToDS and SendModuleDataToDSAsync are members of the same class. You simplay need a instance variable for a SemaphoreSlim and then at the start of each methode that needs synchornization call await lock.WaitAsync() and call lock.Release() in the finally block.

    public async Task SendModuleDataToDSAsync(Module parameters)
    {
        await lock.WaitAsync();
        try
        {
            ...
        }
        finally
        {
            lock.Release();
        }
    }
    
    private async Task SendInstrumentSettingsToDS(<param1>, <param2>)
    {
        await lock.WaitAsync();
        try
        {
            ...
        }
        finally
        {
            lock.Release();
        }
    }
    

    and it is importend that the call to lock.Release() is in the finally-block, so that if an exception is thrown somewhere in the code of the try-block the semaphore is released.

    0 讨论(0)
  • 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<T> would work:

    public sealed class ExecutionQueue
    {
      private readonly BlockingCollection<Func<Task>> _queue = new BlockingCollection<Func<Task>>();
    
      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<Task> lambda)
    {
      var tcs = new TaskCompletionSource<object>();
      _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<Task>();
      var tasks2 = new List<Task>();
    
      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);
    }
    
    0 讨论(0)
  • 2021-01-28 15:07

    One option is to queue operations that will create tasks instead of queuing already running tasks as the code in the question does.

    PseudoCode without locking:

     Queue<Func<Task>> tasksQueue = new Queue<Func<Task>>();
    
     async Task RunAllTasks()
     {
          while (tasksQueue.Count > 0)
          { 
               var taskCreator = tasksQueue.Dequeu(); // get creator 
               var task = taskCreator(); // staring one task at a time here
               await task; // wait till task completes
          }
      }
    
      // note that declaring createSaveModuleTask does not  
      // start SaveModule task - it will only happen after this func is invoked
      // inside RunAllTasks
      Func<Task> createSaveModuleTask = () => SaveModule(setting);
    
      tasksQueue.Add(createSaveModuleTask);
      tasksQueue.Add(() => SaveModule(GetAdvancedData(setting)));
      // no DB operations started at this point
    
      // this will start tasks from the queue one by one.
      await RunAllTasks();
    

    Using ConcurrentQueue would be likely be right thing in actual code. You also would need to know total number of expected operations to stop when all are started and awaited one after another.

    0 讨论(0)
  • 2021-01-28 15:10

    Please keep in mind that your first solution queueing all tasks to lists doesn't ensure that the tasks are executed one after another. They're all running in parallel because they're not awaited until the next tasks is startet.

    So yes you've to use a SemapohoreSlim to use async locking and await. A simple implementation might be:

    private readonly SemaphoreSlim _syncRoot = new SemaphoreSlim(1);
    
    public async Task SendModuleDataToDSAsync(Module parameters)
    {
        await this._syncRoot.WaitAsync();
        try
        {
            foreach (var setting in Module.param)
            {
               await SaveModule(setting);
               await SaveModule(GetAdvancedData(setting));
            }
        }
        finally
        {
            this._syncRoot.Release();
        }
    }
    

    If you can use Nito.AsyncEx the code can be simplified to:

    public async Task SendModuleDataToDSAsync(Module parameters)
    {
        using var lockHandle = await this._syncRoot.LockAsync();
    
        foreach (var setting in Module.param)
        {
           await SaveModule(setting);
           await SaveModule(GetAdvancedData(setting));
        }
    }
    
    0 讨论(0)
提交回复
热议问题