简体   繁体   English

C#中排队异步任务

[英]Queuing asynchronous task in C#

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 make sure that these DS calls are executed one after another in order at any given time.这些对数据服务的调用都结束了,因此我们要确保这些 DS 调用在任何给定时间都按顺序执行。 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.最初,我对这些方法中的每一个都使用了 async await 并且每个调用都是异步执行的,但我们发现如果它们顺序不正确,那么就有错误的余地。

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”。 Will this be appropriate in my use case?这适合我的用例吗? Or what other options will suit my use case?或者还有哪些其他选项适合我的用例? Please, guide me.请指导我。

So, what i have in my code currently所以,我目前的代码中有什么

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

//somewhere down the main thread, invoking second call 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
               });
    }
 }

在此处输入图像描述

在此处输入图像描述

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.最初,我在这些方法中的每一个上都使用了 async await,并且每个调用都是异步执行的,但是我们发现如果它们乱序,那么就有出错的空间。

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' 。

SemaphoreSlim does restrict asynchronous code to running one at a time , and is a valid form of mutual exclusion . SemaphoreSlim确实将异步代码限制为一次运行一个,并且是一种有效的互斥形式。 However, since "out of sequence" calls can cause errors, then SemaphoreSlim is not an appropriate solution since it does not guarantee FIFO.但是,由于“乱序”调用会导致错误,因此SemaphoreSlim不是合适的解决方案,因为它不保证 FIFO。

In a more general sense, no synchronization primitive guarantees FIFO because that can cause problems due to side effects like lock convoys.从更一般的意义上讲,没有同步原语能保证 FIFO,因为这可能会由于锁护航等副作用而导致问题。 On the other hand, it is natural for data structures to be strictly FIFO.另一方面,数据结构很自然地是严格的 FIFO。

So, you'll need to use your own FIFO queue, rather than having an implicit execution queue.因此,您需要使用自己的 FIFO 队列,而不是使用隐式执行队列。 Channels is a nice, performant, async-compatible queue, but since you're on an older version of C#/.NET, BlockingCollection<T> would work: Channels 是一个不错的、高性能的、异步兼容的队列,但由于您使用的是旧版本的 C#/.NET, BlockingCollection<T>可以工作:

public sealed class ExecutionQueue
{
  private readonly BlockingCollection<Func<Task>> _queue = new BlockingCollection<Func<Task>>();

  public ExecutionQueue() => Completion = 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.从排队工作的代码的角度来看,他们想知道 lambda 何时执行,而不是 lambda 何时排队。 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.从 queue 方法(我称之为Run )的角度来看,该方法只需要在 lambda 执行后完成其返回的任务。 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.关于OperationCanceledException处理还有一个边缘情况。 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);
}

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.所以是的,你必须使用SemapohoreSlim来使用异步锁定和等待。 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:如果您可以使用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));
    }
}

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.在实际代码中使用ConcurrentQueue可能是正确的。 You also would need to know total number of expected operations to stop when all are started and awaited one after another.您还需要知道当所有操作都开始并一个接一个地等待时要停止的预期操作总数。

Building on your comment under Alexeis answer, your approch with the SemaphoreSlim is correct.根据您在 Alexeis 回答下的评论,您使用SemaphoreSlim是正确的。

Assumeing that the methods SendInstrumentSettingsToDS and SendModuleDataToDSAsync are members of the same class.假设方法SendInstrumentSettingsToDSSendModuleDataToDSAsync是同一类的成员。 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.您 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();
    }
}

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.重要的是,对lock.Release()的调用是在 finally 块中的,这样如果在 try 块的代码中的某处抛出异常,信号量就会被释放。

Here's a compact solution that has the least amount of moving parts but still guarantees FIFO ordering (unlike some of the SemaphoreSlim solutions above).这是一个紧凑的解决方案,它具有最少的移动部件,但仍保证 FIFO 排序(与上面的一些 SemaphoreSlim 解决方案不同)。 There are two overloads for Enqueue so you can enqueue tasks with and without return values. Enqueue 有两个重载,因此您可以将带有和不带返回值的任务排入队列。

using System;
using System.Threading;
using System.Threading.Tasks;

public class TaskQueue
{
    private Task _previousTask = Task.CompletedTask;

    public Task Enqueue(Func<Task> func)
    {
        return Enqueue(async () => { await func(); return true; });
    }

    public async Task<T> Enqueue<T>(Func<Task<T>> func)
    {
        var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        // get predecessor and wait until it's done. Also atomically swap in our own completion task.
        await Interlocked.Exchange(ref _previousTask, tcs.Task).ConfigureAwait(false);
        try
        { 
            return await func().ConfigureAwait(false); 
        }
        finally 
        { 
            tcs.SetResult(); 
        }
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM