在C#中排队异步任务

时间:2020-04-02 04:12:18

标签: c# asynchronous async-await queue semaphore

我很少有方法可以向数据库报告一些数据。我们要异步调用对数据服务的所有调用。这些对数据服务的调用已经结束,因此我们要确保在任何给定时间依次执行这些DS调用。最初,我在每个方法上都使用async等待,并且每个调用都是异步执行的,但我们发现它们是否不正确,那么就有出错的余地。

所以,我认为我们应该将所有这些异步任务排队,并在单独的线程中发送它们,但是我想知道我们有哪些选择?我遇到了“ SemaphoreSlim”。这对我的用例合适吗? 或者还有哪些其他选项适合我的用例?请指导我。

所以,我目前在代码中拥有什么

public static SemaphoreSlim mutex = new SemaphoreSlim(1);

//first DS call 

 public async Task SendModuleDataToDSAsync(Module parameters)
    {
        var tasks1 = new List<Task>();
        var tasks2 = new List<Task>();

        //await mutex.WaitAsync(); **//is this correct way to use SemaphoreSlim ?**
        foreach (var setting in Module.param)
        {
           Task job1 = SaveModule(setting);
           tasks1.Add(job1);
           Task job2= SaveModule(GetAdvancedData(setting));
           tasks2.Add(job2);
        }

        await Task.WhenAll(tasks1);
        await Task.WhenAll(tasks2);

        //mutex.Release(); // **is this correct?**
    }

 private async Task SaveModule(Module setting)
    {
        await Task.Run(() =>
            {
             // Invokes Calls to DS
             ... 
            });
    }

///在主线程中的某个地方,调用了对DS的第二次调用

  //Second DS Call
 private async Task SendInstrumentSettingsToDS(<param1>, <param2>)
 {
    //await mutex.WaitAsync();// **is this correct?**
    await Task.Run(() =>
            {
                 //TrackInstrumentInfoToDS
                 //mutex.Release();// **is this correct?**
            });
    if(param2)
    {
        await Task.Run(() =>
               {
                  //TrackParam2InstrumentInfoToDS
               });
    }
 }

enter image description here

enter image description here

4 个答案:

答案 0 :(得分:3)

最初,我在每种方法上都使用了异步等待,并且每个调用都是异步执行的,但是我们发现它们是否不正确,那么就有出错的余地了。

所以,我认为我们应该将所有这些异步任务排队,并在单独的线程中发送它们,但是我想知道我们有哪些选择?我遇到了“ SemaphoreSlim”。

SemaphoreSlim确实将异步代码限制为一次只能运行 ,并且是互斥的有效形式。但是,由于“乱序”调用可能导致错误,因此SemaphoreSlim 不是合适的解决方案,因为它不能保证FIFO。

从更一般的意义上讲,没有任何同步原语可以保证FIFO,因为这可能由于诸如锁定车队之类的副作用而引起问题。另一方面,数据结构严格地采用FIFO是很自然的。

因此,您需要使用自己的FIFO队列,而不要使用隐式执行队列。通道是一个不错的,高性能的,异步兼容的队列,但是由于您使用的是C#/。NET的旧版本,因此BlockingCollection<T>可以正常工作:

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();
  }
}

此设置唯一棘手的部分是如何对工作进行排队。从代码排队的角度来看,他们想知道何时执行lambda,而不是何时将lambda排队。从队列方法的角度(我称其为Run),该方法仅在执行lambda之后才需要完成其返回的任务。因此,您可以编写以下队列方法:

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;
}

这种排队方法并不尽如人意。如果一个任务完成并有多个异常(这对于并行代码而言是正常的),则仅保留第一个异常(对于异步代码而言这是正常的)。 OperationCanceledException处理方面也有一个极端案例。但是这段代码对于大多数情况已经足够了。

现在您可以像这样使用它:

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);
}

答案 1 :(得分:1)

请记住,将所有任务排队到列表的第一个解决方案不能确保任务一次又一次地执行。它们都是并行运行的,因为直到下一个任务开始才等待它们。

是的,您必须使用SemapohoreSlim来使用异步锁定并等待。一个简单的实现可能是:

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();
    }
}

如果您可以使用Nito.AsyncEx,则代码可以简化为:

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));
    }
}

答案 2 :(得分:0)

一种选择是对将创建任务的操作进行排队,而不是像问题中的代码那样对已在运行的任务进行排队。

没有锁定的伪代码:

 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();

在实际代码中使用ConcurrentQueue可能是正确的选择。您还需要知道在全部启动并逐个等待时停止的预期操作总数。

答案 3 :(得分:0)

基于您在Alexeis回答下的评论,您对SemaphoreSlim的批准是正确的。

假设方法SendInstrumentSettingsToDSSendModuleDataToDSAsync是同一类的成员。您simplay需要一个SemaphoreSlim的实例变量,然后在每个需要同步化的方法的开始处调用await lock.WaitAsync(),并在finally块中调用lock.Release()

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();
    }
}

重要的是,对lock.Release()的调用位于finally块中,因此,如果在try块的代码中某处引发了异常,则将释放信号量。