如何将 LongRun 标志专门传递给 Task.Run()

本文关键字:Task Run LongRun 标志 | 更新日期: 2023-09-27 18:30:23

我需要一种方法来设置异步任务,而无需使用Task.Factory.StartNew(...),而是使用Task.Run(...)或类似的东西。

上下文:

我有一个连续循环的任务,直到它被外部取消,我想设置为"长时间运行"(即给它一个专用线程)。这可以通过以下代码实现:

var cts = new CancellationTokenSource();
Task t = Task.Factory.StartNew(
async () => {
while (true)
{
    cts.Token.ThrowIfCancellationRequested();
    try
    {
        "Running...".Dump();
        await Task.Delay(500, cts.Token);
    }
    catch (TaskCanceledException ex) { }
} }, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);

问题是 Task.Factory.StartNew(...) 不会返回传入的活动异步任务,而是返回一个"运行操作的任务",该任务在功能上始终具有"RanToCompletion"的 taskState。由于我的代码需要能够跟踪任务的状态以查看它何时变为"已取消"(或"出错"),因此我需要使用如下所示的内容:

var cts = new CancellationTokenSource();
Task t = Task.Run(
async () => {
while (true)
{
    cts.Token.ThrowIfCancellationRequested();
    try
    {
        "Running...".Dump();
        await Task.Delay(500, cts.Token);
    }
    catch (TaskCanceledException ex) { }
} }, cts.Token);

Task.Run(...),如愿以偿,返回异步进程本身,允许我获得"已取消"或"出错"的实际状态。但是,我无法将任务指定为长时间运行。那么,任何人都知道如何最好地运行异步任务,同时存储活动任务本身(具有所需的 taskStatus)并将任务设置为长时间运行?

如何将 LongRun 标志专门传递给 Task.Run()

我有一个连续循环的任务,直到它被外部取消,我想设置为"长时间运行"(即给它一个专用线程)......任何人都知道如何最好地运行异步任务,同时存储活动任务本身(具有所需的 taskStatus)并将任务设置为长时间运行?

这有几个问题。首先,"长时间运行"并不一定意味着专用线程 - 它只是意味着您向 TPL 提供任务长时间运行的提示。在当前的 (4.5) 实现中,您将获得一个专用线程;但这并不能保证,将来可能会改变。

因此,如果您需要专用线程,则必须创建一个。

另一个问题是"异步任务"的概念。在线程池上运行async代码实际发生的情况是,在异步操作(即 Task.Delay)正在进行时,线程返回到线程池。然后,当异步操作完成时,将从线程池中获取一个线程以恢复async方法。在一般情况下,这比专门保留线程来完成该任务更有效。

因此,由于线程池上运行async任务,专用线程实际上没有意义。


关于解决方案:

如果您确实需要一个专用线程来运行async代码,我建议您使用我的 AsyncEx 库中的AsyncContextThread

using (var thread = new AsyncContextThread())
{
  Task t = thread.TaskFactory.Run(async () =>
  {
    while (true)
    {
      cts.Token.ThrowIfCancellationRequested();
      try
      {
        "Running...".Dump();
        await Task.Delay(500, cts.Token);
      }
      catch (TaskCanceledException ex) { }
    }
  });
}

但是,您几乎肯定不需要专用线程。如果您的代码可以在线程池上执行,那么它可能应该;专用线程对于线程池上运行async方法没有意义。更具体地说,长时间运行的标志对于线程池上运行async方法没有意义。

换句话说,对于async lambda,线程池实际执行(并视为任务)只是 await 语句之间的 lambda 部分。由于这些部分不是长时间运行的,因此不需要长时间运行的标志。您的解决方案将变为:

Task t = Task.Run(async () =>
{
  while (true)
  {
    cts.Token.ThrowIfCancellationRequested(); // not long-running
    try
    {
      "Running...".Dump(); // not long-running
      await Task.Delay(500, cts.Token); // not executed by the thread pool
    }
    catch (TaskCanceledException ex) { }
  }
});
调用Unwrap返回

的任务Task.Factory.StartNew这将返回具有正确状态的内部任务。

var cts = new CancellationTokenSource();
Task t = Task.Factory.StartNew(
async () => {
while (true)
{
    cts.Token.ThrowIfCancellationRequested();
    try
    {
        "Running...".Dump();
        await Task.Delay(500, cts.Token);
    }
    catch (TaskCanceledException ex) { }
} }, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap();

在专用线程上,没有什么可屈服的。 不要使用asyncawait,使用同步调用。

这个问题给出了两种在没有await的情况下进行可取消睡眠的方法:

Task.Delay(500, cts.Token).Wait(); // requires .NET 4.5
cts.WaitHandle.WaitOne(TimeSpan.FromMilliseconds(500)); // valid in .NET 4.0 and later

如果您的部分工作确实使用并行性,则可以启动并行任务,将这些任务保存到数组中,并在Task[]上使用Task.WaitAny。 在主线程过程中仍然没有用await

这是

不必要的,Task.Run 就足够了,因为如果任何任务运行超过 0.5 秒,任务计划程序会将任何任务设置为 LongRun。

请参阅此处原因。https://blog.stephencleary.com/2013/08/startnew-is-dangerous.html

您需要指定自定义任务创建选项。让我们考虑一下 选项。AttachedToParent 不应该在异步任务中使用,所以 这就出来了。DenyChildAttach 应始终与异步任务一起使用 (提示:如果你还不知道,那么StartNew不是工具。 你需要)。DenyChildAttach 由 Task.Run 传递。隐藏调度程序可能 在一些非常晦涩的调度场景中很有用,但总的来说 应避免用于异步任务。那只剩下长跑和 PreferFairness,两者都是优化提示,应该只 在应用程序分析之后指定。我经常看到 LongRun 被滥用 特别。在绝大多数情况下,线程池将 在 0.5 秒内调整到任何长时间运行的任务 - 没有 长跑旗。最有可能的是,你并不真正需要它。

您在这里遇到的真正问题是您的操作实际上并没有长时间运行。 您正在执行的实际工作是异步操作,这意味着它基本上会立即返回给调用方。 因此,在让任务调度程序调度它时,您不仅不需要使用长时间运行的提示,甚至不需要使用线程池线程来完成这项工作,因为它基本上是即时的。 您根本不应该使用StartNewRun,更不用说长时间运行的标志了。

因此,与其采用异步方法

并在另一个线程中启动它,不如通过调用异步方法直接在当前线程上启动它。 卸载已经异步的操作的启动只是创建更多的工作,使事情变慢

因此,您的代码可以简化为:

var cts = new CancellationTokenSource();
Task t = DoWork();
async Task DoWork()
{
    while (true)
    {
        cts.Token.ThrowIfCancellationRequested();
        try
        {
            "Running...".Dump();
            await Task.Delay(500, cts.Token);
        }
        catch (TaskCanceledException) { }
    }
}
我认为考虑的

不是线程运行多长时间,而是它真正工作了多少时间。在您的示例中,工作很短,它们await Task.Delay(...).如果您的项目中确实是这种情况,您可能不应该为此任务使用专用线程,而应该让它在常规线程池上运行。每次在 IO 操作或Task.Delay()上调用await时,都会释放线程以供其他任务使用。

您应该只在线程池中减少线程时才使用LongRunning,并且永远不要回馈或仅在一小部分时间内回馈。在这种情况下(相比之下,工作很长,Task.Delay(...)更短),为作业使用专用线程是一个合理的解决方案。另一方面,如果您的线程大部分时间都在工作,它将消耗系统资源(CPU 时间),并且它可能是否持有线程池的线程并不重要,因为它无论如何都会阻止其他工作发生。

结论?只需使用Task.Run()(不带LongRunning),并在可能的情况下在长时间运行的任务中使用await。仅当您实际看到其他方法给您带来问题时,才恢复到LongRunning,即使这样,请检查您的代码和设计以确保它确实有必要,并且您的代码中没有其他可以更改的内容。