一个接一个地执行非阻塞异步任务

c#

我正在使用 C#、TPL。我有一个类包含一些执行一些子任务的异步方法,为简单起见,我将只考虑一种方法和一个子任务:

class Test1
{
    private Task SubTask() => Task.Delay(1000);

    public async Task FullTask()
    {
        Console.WriteLine("Task Start");
        await SubTask();
        Console.WriteLine("Task Middle");
        await SubTask();
        Console.WriteLine("Task End");
    }

    static async Task Main()
    {
        Test1 Test = new Test1();
        Task Task1 = Test.FullTask();
        Task Task2 = Test.FullTask();
        await Task.WhenAll(Task1, Task2);
    }
}

执行后,控制台中会打印以下(预期的)结果:

Task Start
Task Start
Task Middle
Task Middle
Task End
Task End

问题是每次调用都FullTask必须在前一个调用完成后运行,如果多个调用FullTask同时发生,则必须逐个处理。我的第一个想法是使用该ContinueWith方法:

class Test2
{
    private Task LastTask = Task.CompletedTask;

    private Task SubTask() => Task.Delay(1000);

    public Task FullTask()
    {
        lock(LastTask)
        {
            return LastTask = LastTask.ContinueWith(_ =>
            {
                Console.WriteLine("Task Start");
                SubTask().Wait();
                Console.WriteLine("Task Middle");
                SubTask().Wait();
                Console.WriteLine("Task End");
            });
        }
    }

    static async Task Main()
    {
        Test2 Test = new Test2();
        Task Task1 = Test.FullTask();
        Task Task2 = Test.FullTask();
        await Task.WhenAll(Task1, Task2);
    }
}

同样,在执行时,以下(预期的)结果会打印在控制台中:

Task Start
Task Middle
Task End
Task Start
Task Middle
Task End

问题是内部的 lambdaFullTask阻塞了线程,因为它使用SubTask().Wait();而不是await SubTask();. 如果Test2该类存在多个实例,每个实例都执行该FullTask方法,则会发生线程池饥饿。将 etherFullTask或 lambda(或两者)更改为 async 并不能解决问题:

class Test3
{
    private Task LastTask = Task.CompletedTask;

    private Task SubTask() => Task.Delay(1000);

    public Task FullTask()
    {
        lock(LastTask)
        {
            return LastTask = LastTask.ContinueWith(async _ =>
            {
                Console.WriteLine("Task Start");
                await SubTask();
                Console.WriteLine("Task Middle");
                await SubTask();
                Console.WriteLine("Task End");
            });
        }
    }

    static async Task Main()
    {
        Test3 Test = new Test3();
        Task Task1 = Test.FullTask();
        Task Task2 = Test.FullTask();
        await Task.WhenAll(Task1, Task2);
    }
}

ContinueWith回报Task<Task>,外Task为计划任务后执行LastTask。该任务将在第一次等待时结束,并将返回将在 lambda 结束时结束的内部(编译器生成)任务。在这里,我与在外部任务到达第一个等待之前未创建的内部任务相交。因此方法不起作用。

我想要的是一种非阻塞方法,它产生与Test. 有任何想法吗?

回答

如果您想要该ContinueWith方法的迭代(使用更现代的await),则应该使用以下方法:

private readonly object _lastTaskMutex = new object();
public Task FullTask()
{
  lock (_lastTaskMutex)
    return LastTask = RunAfterAsync(LastTask);

  async Task RunAfterAsync(Task lastTask)
  {
    try
    {
      await lastTask;
    }
    catch { }
    Console.WriteLine("Task Start");
    await SubTask();
    Console.WriteLine("Task Middle");
    await SubTask();
    Console.WriteLine("Task End");
  }
}

如果您只关心互斥并且确切的顺序无关紧要,那么 aSemaphoreSlim会起作用:

private readonly SemaphoreSlim _mutex = new SemaphoreSlim(1);
public async Task FullTask()
{
  await _mutex.WaitAsync();
  try
  {
    Console.WriteLine("Task Start");
    await SubTask();
    Console.WriteLine("Task Middle");
    await SubTask();
    Console.WriteLine("Task End");
  }
  finally
  {
    _mutex.Release();
  }
}

或者,如果您真正想要的是严格的 FIFO 操作队列,那么 Channel 或ActionBlockPatagonias 建议的那样是合适的;在这种情况下,您通常希望通过 aTaskCompletionSource<T>来指示单个请求何时完成:

private readonly ActionBlock<TaskCompletionSource<object>> _block = new ActionBlock<TaskCompletionSource<object>>(async tcs =>
{
  try
  {
    Console.WriteLine("Task Start");
    await SubTask();
    Console.WriteLine("Task Middle");
    await SubTask();
    Console.WriteLine("Task End");
    tcs.TrySetResult(null);
  }
  catch (Exception ex)
  {
    tcs.TrySetException(ex);
  }
});

public Task FullTask()
{
  var tcs = new TaskCompletionSource<object>();
  _block.Post(tcs);
  return tcs.Task;
}


以上是一个接一个地执行非阻塞异步任务的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>