【问题标题】:Does or does Task.Wait not start the task if it isn't already running?如果 Task.Wait 尚未运行,它是否不启动任务?
【发布时间】:2018-11-05 14:22:31
【问题描述】:

根据我阅读的 Microsoft TPL 文档 (link),调用 Task.Wait() 方法将阻塞当前线程,直到该任务完成(或取消或出错)。但它也表示,如果有问题的任务尚未开始,Wait 方法将尝试通过要求调度程序重新分配它来在自己的线程上运行它,从而减少阻塞造成的浪费。

我有一个系统,其中任务(一旦运行)通过启动其他任务并等待其结果来收集数据。这些其他任务依次从其他任务中收集数据开始,可能有几百层深。我真的不希望无数任务阻塞并等待最后一个任务最终完成。

但是,当我在测试控制台应用程序中尝试这个时,Task.Wait() 似乎根本没有启动任何东西。

构建必须以最少浪费周期相互等待的任务序列的正确咒语是什么?它有点像 ContinueWith,除了从系列中的最后一个任务开始......

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

namespace ConsoleApp1
{
  class Program
  {
    static void Main(string[] args)
    {
      var source = new CancellationTokenSource();
      var token = source.Token;

      // Create a non-running task.
      var task = new Task<string[]>(() => InternalCompute(token), token);

      // Isn't this supposed to start the task?
      task.Wait(CancellationToken.None);

      // I realise this code now won't run until the task finishes,
      // it's here for when I use task.Start() instead of task.Wait().
      Console.WriteLine("Press any key to cancel the process.");
      Console.ReadKey(true);
      source.Cancel();
      Console.WriteLine("Source cancelled...");

      Console.WriteLine("Press any key to quit.");
      Console.ReadKey(true);
    }

    private static string[] InternalCompute(CancellationToken token)
    {
      string[] data;
      try
      {
        data = Compute(token);
      }
      catch (TaskCanceledException ex)
      {
        return null;
      }
      catch (Exception ex)
      {
        Console.WriteLine(ex.Message);
        return new[] { ex.Message };
      }

      Console.WriteLine("Post-processor starting.");
      for (int i = 0; i < data.Length; i++)
        if (data[i] is null)
          Console.WriteLine($"Null data at {i}.");
        else
          Console.WriteLine($"Valid data at {i}.");
      Console.WriteLine("Post-processor completed.");
      return data;
    }

    /// <summary>
    /// This method stands in for an abstract one to be implemented by plug-in developers.
    /// </summary>
    private static string[] Compute(CancellationToken token)
    {
      var data = new string[10];
      for (int i = 0; i < data.Length; i++)
      {
        token.ThrowIfCancellationRequested();
        Thread.Sleep(250);
        data[i] = i.ToString();
        Console.WriteLine($"Computing item {i + 1}...");
      }
      return data;
    }
  }
}

【问题讨论】:

  • 你试过 Run() 代替吗? Task.Run(() => ());
  • 根据this documentationTask.Wait() onlyTask 等待,仅此而已。您需要使用Task.Run()Task.Start()
  • 另外,如果您不使用异步,则不需要任务或 CancellationToken。
  • @Tau,找到我之前看过的文档:blogs.msdn.microsoft.com/pfxteam/2009/10/15/…
  • @fstam,如果我先 Run() 任务,然后 Wait() 任务,在任务在其他线程上完成之前,我当前的代码不会阻塞吗?如果是这样,我想尽可能避免这种情况。

标签: c# task-parallel-library


【解决方案1】:

Tasks 通常分为两组——“冷”任务和“热”任务。 “冷”任务是尚未启动且不打算运行的任务。 “热”任务是当前可能正在运行或未运行的任务,但重要的是,如果它们尚未运行,它们可能随时运行。它们意味着正在运行,但尚未分配到它们需要这样做的资源(线程)。

this post 所说的是执行一个原本没有机会运行的“热”任务。 “热”任务是通过调用例如创建的。 Task.Run()。他们也是例如您将从异步方法收到的Tasks 的类型。 new Task(...),另一方面给你“冷”任务。除非或直到您在该任务上调用 Start 或道德等效方法,否则它仍然是“冷的”。它显式调用其中一种方法,使其“热”而不是“冷”。

通常,这些天您不想一直在处理“冷”任务,这就是为什么直接调用 Task 构造函数不被接受的原因。在他们弄清楚调度应该如何真正工作之前,他们真的是一个糟糕的实验。大多数现代代码根本不希望处理“冷”任务。

上面帖子的关键引述是这个:

但是,如果它还没有开始执行,Wait 可能能够将目标任务从其排队的调度程序中拉出并在当前线程上内联执行。

如果您没有在任务上调用Start,则它还没有被调度程序排队 - 所以显然我们不能按照上面所说的那样做。

【讨论】:

  • 是的,现在这一切终于有了意义。 Task.Wait() 优化不是 api 设计的一个方面。听起来我要么必须开始使用 async-await 要么直接内联。
【解决方案2】:

这是the article 中引起混淆的部分(重点已添加)。

如果正在等待的任务已经开始执行,则等待必须阻塞。但是,如果它还没有开始执行,Wait 可能能够将目标任务从它排队的调度程序中拉出来,并在当前线程上内联执行它。

这是Task.Start方法的描述:

启动Task,安排它执行到当前TaskScheduler

这些是任务生命周期的eight different stages

public enum TaskStatus
{
    Created = 0, // The task has been initialized but has not yet been scheduled.
    WaitingForActivation = 1, // The task is waiting to be activated and scheduled internally by the .NET Framework infrastructure.
    WaitingToRun = 2, // The task has been scheduled for execution but has not yet begun executing.
    Running = 3, // The task is running but has not yet completed.
    WaitingForChildrenToComplete = 4, // The task has finished executing and is implicitly waiting for attached child tasks to complete.
    RanToCompletion = 5, // The task completed execution successfully.
    Canceled = 6, // The task acknowledged cancellation by throwing an OperationCanceledException with its own CancellationToken while the token was in signaled state, or the task's CancellationToken was already signaled before the task started executing.
    Faulted = 7 // The task completed due to an unhandled exception.
}

文章含蓄地讨论了处于WaitingForActivationWaitingToRun 阶段的 任务,并解释了在哪些条件下它们可以在内部推进到Running 阶段当他们的Wait 方法被调用时。这不是在谈论Created 阶段中​​的任务。如果没有调用其StartRunSynchronously 方法,则此阶段的任务无法进行。换句话说,.NET 基础架构永远不会自动将任务的温度从冷变为热。此责任 100% 属于使用 Task constructor 创建任务的应用程序代码。

作为旁注,值得从评论中引用这句话:

此构造函数只应在需要将任务的创建和启动分开的高级场景中使用。

【讨论】:

    最近更新 更多