c# - 在 ContinueWith block 内使用 await 的意外行为

标签 c# multithreading asynchronous async-await

我有一个稍微复杂的要求,即并行执行一些任务,并且必须等待其中一些任务完成才能继续。现在,当我有许多任务要并行执行时,我遇到了意外行为,但在 ContinueWith 处理程序中。我准备了一个小样本来说明问题:

var task1 = Task.Factory.StartNew(() =>
{
    Console.WriteLine("11");
    Thread.Sleep(1000);
    Console.WriteLine("12");
}).ContinueWith(async t =>
{
    Console.WriteLine("13");
    var innerTasks = new List<Task>();
    for (var i = 0; i < 10; i++)
    {
        var j = i;
        innerTasks.Add(Task.Factory.StartNew(() =>
        {
            Console.WriteLine("1_" + j + "_1");
            Thread.Sleep(500);
            Console.WriteLine("1_" + j + "_2");
        }));
    }
    await Task.WhenAll(innerTasks.ToArray());
    //Task.WaitAll(innerTasks.ToArray());
    Thread.Sleep(1000);
    Console.WriteLine("14");
});
var task2 = Task.Factory.StartNew(() =>
{
    Console.WriteLine("21");
    Thread.Sleep(1000);
    Console.WriteLine("22");
}).ContinueWith(t =>
{
    Console.WriteLine("23");
    Thread.Sleep(1000);
    Console.WriteLine("24");
});
Console.WriteLine("1");
await Task.WhenAll(task1, task2);
Console.WriteLine("2");

基本模式是: - 任务 1 应与任务 2 并行执行。 - 完成第 1 部分的第一部分后,它应该并行执行更多操作。我想完成,一旦一切都完成了。

我希望得到以下结果:

1 <- Start
11 / 21 <- The initial task start
12 / 22 <- The initial task end
13 / 23 <- The continuation task start
Some combinations of "1_[0..9]_[1..2]" and 24 <- the "inner" tasks of task 1 + the continuation of task 2 end
14 <- The end of the task 1 continuation
2 <- The end

相反,await Task.WhenAll(innerTasks.ToArray()); 不会“阻止”继续任务完成。因此,内部任务在外部 await Task.WhenAll(task1, task2); 完成后执行。结果是这样的:

1 <- Start
11 / 21 <- The initial task start
12 / 22 <- The initial task end
13 / 23 <- The continuation task start
Some combinations of "1_[0..9]_[1..2]" and 24 <- the "inner" tasks of task 1 + the continuation of task 2 end
2 <- The end
Some more combinations of "1_[0..9]_[1..2]" <- the "inner" tasks of task 1
14 <- The end of the task 1 continuation

相反,如果我使用 Task.WaitAll(innerTasks.ToArray()),一切似乎都按预期工作。当然,我不想使用 WaitAll,所以我不会阻塞任何线程。

我的问题是:

  1. 为什么会发生这种意外行为?
  2. 如何在不阻塞任何线程的情况下补救这种情况?

非常感谢您的指点!

最佳答案

您使用了错误的工具。代替 StartNew,使用 Task.Run。代替 ContinueWith,使用 await:

var task1 = Task1();
var task2 = Task2();
Console.WriteLine("1");
await Task.WhenAll(task1, task2);
Console.WriteLine("2");

private async Task Task1()
{
  await Task.Run(() =>
  {
    Console.WriteLine("11");
    Thread.Sleep(1000);
    Console.WriteLine("12");
  });
  Console.WriteLine("13");
  var innerTasks = new List<Task>();
  for (var i = 0; i < 10; i++)
  {
    innerTasks.Add(Task.Run(() =>
    {
      Console.WriteLine("1_" + i + "_1");
      Thread.Sleep(500);
      Console.WriteLine("1_" + i + "_2");
    }));
    await Task.WhenAll(innerTasks);
  }
  Thread.Sleep(1000);
  Console.WriteLine("14");
}

private async Task Task2()
{
  await Task.Run(() =>
  {
    Console.WriteLine("21");
    Thread.Sleep(1000);
    Console.WriteLine("22");
  });
  Console.WriteLine("23");
  Thread.Sleep(1000);
  Console.WriteLine("24");
}

Task.Runawait 在这里更优越,因为它们纠正了 StartNew/ContinueWith 中的许多意外行为>。特别是,异步委托(delegate)和(对于 Task.Run)总是使用线程池。

我的博客上有关于 why you shouldn't use StartNew 的更多详细信息和 why you shouldn't use ContinueWith .

关于c# - 在 ContinueWith block 内使用 await 的意外行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40136943/

相关文章:

c# - 配置 VS 2010,使其不添加 new EventHandler(...);通过 += 添加 EventHandler 时

c# - 调用方法时结构体值不会改变

c# - 在 VSTO 中使用异步/等待跨线程操作无效

c++ - WaitForSingleObject - 每个线程一次

c++ - 我可以有一个 lambda 的 boost 无锁队列吗?

entity-framework - Entity Framework 6 中的多异步?

c# - 创建异步方法并设置其他方法的任务结果

c# - 模型验证 ASP.NET Core MVC 的自定义错误消息

c# - Entity Framework 的 SQL 运算符函数对 SQL 注入(inject)安全吗?

python - 规则定义中 "threads"通配符的算术运算