如何将 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)并将任务设置为长时间运行?
我有一个连续循环的任务,直到它被外部取消,我想设置为"长时间运行"(即给它一个专用线程)......任何人都知道如何最好地运行异步任务,同时存储活动任务本身(具有所需的 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();
在专用线程上,没有什么可屈服的。 不要使用async
和await
,使用同步调用。
这个问题给出了两种在没有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 秒内调整到任何长时间运行的任务 - 没有 长跑旗。最有可能的是,你并不真正需要它。
您在这里遇到的真正问题是您的操作实际上并没有长时间运行。 您正在执行的实际工作是异步操作,这意味着它基本上会立即返回给调用方。 因此,在让任务调度程序调度它时,您不仅不需要使用长时间运行的提示,甚至不需要使用线程池线程来完成这项工作,因为它基本上是即时的。 您根本不应该使用StartNew
或Run
,更不用说长时间运行的标志了。
并在另一个线程中启动它,不如通过调用异步方法直接在当前线程上启动它。 卸载已经异步的操作的启动只是创建更多的工作,使事情变慢。
因此,您的代码可以简化为:
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
,即使这样,请检查您的代码和设计以确保它确实有必要,并且您的代码中没有其他可以更改的内容。