【问题标题】:Background started task does not finish/gets terminated after the first encountered await后台启动的任务在第一次遇到等待后未完成/终止
【发布时间】:2020-03-28 07:21:42
【问题描述】:

在一个 ASP.NET 应用程序中,我有一个动作,当它被点击时,以下列方式启动一个新的后台任务:

Task.Factory.StartNew(async () => await somethingWithCpuAndIo(input), CancellationToken.None, TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning, TaskScheduler.FromCurrentSynchronizationContext());

我不是在等待它,我只是希望它启动,并继续在后台执行它的工作。 之后,我立即向客户端返回响应。

尽管出于某种原因,在执行后台工作的初始线程遇到等待方法完成的 await 之后,调试时,我成功解析了该方法,但在返回时,执行只是停止并且不会在该点以下继续.

有趣的是,如果我完全等待这个任务(使用双精度 await),一切都会按预期进行。

这是由于 SynchronizationContext 造成的吗?在我返回响应的那一刻,同步上下文被释放/删除? (方法内部使用了 SynchronizationContext)

如果是因为这个,问题到底发生在哪里?

A) 当调度器尝试在给定的同步上下文上分配工作时,它已经被释放,所以不会提供任何东西

B) 在方法执行的某个地方,当我向客户端返回响应时,同步上下文会丢失,不管其他任何事情。

C) 完全不同的东西?

如果是 A),我应该能够通过在安排工作和返回响应之间简单地执行 Thread.Sleep() 来解决此问题。 (试过了,还是不行。)

如果是 B) 我不知道如何解决这个问题。我们将不胜感激。

【问题讨论】:

  • Task.Factory.StartNew(async () => 返回一个Task<Task>。外部任务只是创建内部任务。创建任务通常不是长时间运行的工作。
  • 这是一个有趣的阅读:How to run Background Tasks in ASP.NET。这是另一个:Fire and Forget on ASP.NET.
  • @TheodorZoulias 的意思是我们在使用异步 lambda 时永远不应该传递 Task.LongRunning
  • “从不”是一个强词。通常你应该有no reason 来使用TaskCreationOptions.LongRunning 选项和async lambdas,但可能会有一些罕见的例外。通常,此选项旨在用作优化技术,即"don't use it unless you find that you really need it"。 ????

标签: c# asp.net multithreading asynchronous async-await


【解决方案1】:

正如 Gabriel Luci 所指出的,这是由于第一个等待不完整的Task 立即返回,但关于Task.Factory.StartNew 有更广泛的意义。

Task.Factory.StartNew 不应与 async 代码一起使用,TaskCreationOptions.LongRunning 也不应使用。 TaskCreationOptions.LongRunning 应该用于调度长时间运行的CPU-bound 工作。使用async 方法,它可能在逻辑上长时间运行,但Task.Factory.StartNew 是关于开始同步 工作,异步方法的同步部分是第一个await 之前的位,这是通常很短。

以下是 David Fowler(Microsoft 的 ASP.NET 团队的合作伙伴软件架构师)对此事的指导: https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/86b502e88c752e42f68229afb9f1ac58b9d1fef7/AsyncGuidance.md#avoid-using-taskrun-for-long-running-work-that-blocks-the-thread

查看第三个灯泡:

不要将 TaskCreationOptions.LongRunning 与异步代码一起使用,因为这会 创建一个将在第一次等待后销毁的新线程。

【讨论】:

  • 我正在使用Task.Factory.StartNew 以通过TaskScheduler.FromCurrentSynchronizationContext 将同步上下文传递给工作。如果我使用Task.Run,则使用默认的Task Scheduler,即线程池。
  • 如果你想使用当前的SynchronizationContext,你想做什么?我正在努力想一个你想要的场景。 @TheodorZoulias 我交替使用线程绑定和 CPU 绑定,我应该避免使用这个术语吗?
  • 这是一个 ASP.NET 应用程序。我正在安排一些大的事情,如果在请求中等待,将在客户端使请求本身超时。为避免这种情况,最好的办法是安排大型工作,然后通过其他方式发出信号/发送结果。在长期工作中,我正在访问HttpContext.Current 的部分。我需要那个,否则工作会在空引用的那个点抛出异常。
  • 我认为CPU-bound这个词更成熟。 TBH 我以前从未遇到过线程绑定这个词。
  • @SupportMonica-SpiritBob 我认为这将是有问题的,如果您需要与 Web 请求异步进行的工作,您将不想在以后的工作中触及 HttpContext。我会复制 HttpContext.Current 中的值以供需要并传入。
【解决方案2】:

您的 cmets 使您的意图更加清晰。我认为你想做的是:

  1. 开始任务,不要等待。在后台任务完成之前向客户端返回响应。
  2. 确保somethingWithCpuAndIo 方法可以访问请求上下文。

但是,

  1. 不同的线程不会在同一个上下文中,并且
  2. 一旦第一个await被命中,就会返回一个Task,这也意味着Task.Factory.StartNew返回并且调用方法的执行继续。这意味着响应将返回给客户端。 请求完成后,上下文将被释放。

所以你不能真正做你想做的两件事。有几种方法可以解决这个问题:

首先,您可能根本无法在不同的线程上启动它。这取决于somethingWithCpuAndIo 何时需要访问上下文。如果它只需要第一个await之前的上下文,那么你可以这样做:

public IActionResult MyAction(input) {
    somethingWithCpuAndIo(input); //no await
}

private async Task somethingWithCpuAndIo(SomeThing input) {

    // You can read from the request context here

    await SomeIoRequest().ConfigureAwait(false);

    // Everything after here will run on a ThreadPool thread with no access
    // to the request context.
}

每个异步方法都开始同步运行。当await 被赋予不完整的Task 时,魔法就会发生。所以在这个例子中,somethingWithCpuAndIo 将在请求上下文中的同一个线程上开始执行。当它到达await 时,Task 被返回到MyAction,但它没有等待,所以MyAction 完成执行,并且响应被发送到客户端之前 SomeIoRequest()已完成。但是ConfigureAwait(false) 告诉它我们不需要在相同的上下文中恢复执行,所以somethingWithCpuAndIo 在 ThreadPool 线程上恢复执行。

但这只会在您不需要 somethingWithCpuAndIo 中第一个 await 之后的上下文时对您有所帮助。

您最好的选择仍然是在不同的线程上执行,但将您需要的值从上下文传递到somethingWithCpuAndIo

此外,出于described in detail here 的原因,请使用Task.Run 而不是Task.Factory.StartNew

更新:这很可能会导致不可预知的结果,但您也可以尝试将HttpContext.Current 的引用传递给线程并在新线程中设置HttpContext.Current,如下所示:

var ctx = HttpContext.Current;
Task.Run(async () => {
    HttpContext.Current = ctx;
    await SomeIoRequest();
});

但是,这完全取决于您如何使用上下文。 HttpContext 本身并没有实现 IDiposable,所以它本身不能被释放。只要您持有对它的引用,垃圾收集器就不会摆脱它。但是上下文的设计并不比请求的寿命长。因此,在将响应返回给客户端后,可能会有很多 部分 上下文被释放或不可用。测试一下,看看会发生什么爆炸。但即使现在没有发生任何爆炸,您(或其他人)可能稍后会回到该代码,尝试在上下文中使用其他内容,并且当它爆炸时会感到非常困惑。这可能会导致一些难以调试的场景。

【讨论】:

  • 谢谢加布里埃尔!我不确定您是否在我的回答中看到它,但应用程序是 ASP.NET。我记得在某处读过一些东西,说任何产生的线程默认情况下都被认为是后台线程,这意味着当主应用程序停止时,所有后台线程都会自动终止。如果是前台线程,则不会。但是该逻辑如何应用于 ASP.NET 请求线程?请求线程返回的那一刻,ASP.NET 终止所有后台线程,这是怎么回事?我真的只是想在后台启动一些东西,然后返回一个响应。
  • 如果这不可能,有没有办法让它成为可能?我什至尝试将任务存储在我认为可行的静态列表中,但同步上下文丢失了。如果你能在这里帮助我,并验证我的话,那就太好了! TL;DR 有没有办法安排要完成的工作,之后我们向发出请求的客户端返回响应,但该工作仍然能够获取已处理请求的同步上下文(并继续做它的东西)?
  • 对不起,我错过了你提到的 ASP.NET。但是你们这些cmets确实让它更清楚了一点。我重写了我的答案。
  • 关于设置HttpContext.Current,Stephen Cleary 可能会不赞成:HttpContext.Current 永远不应该分配给。 (citation)
  • @TheodorZoulias 我同意。我也不会这样做。
猜你喜欢
  • 2016-08-12
  • 1970-01-01
  • 1970-01-01
  • 2021-08-14
  • 2016-03-06
  • 1970-01-01
  • 1970-01-01
  • 2016-02-23
相关资源
最近更新 更多