【问题标题】:Why a blocking call inside BackgroundService.ExecuteAsync is not blocking the main thread?为什么 BackgroundService.ExecuteAsync 中的阻塞调用不会阻塞主线程?
【发布时间】:2021-10-19 20:47:42
【问题描述】:

我在 Asp.Net Core Web API 中实现了一个 Microsoft.Extensions.Hosting.BackgroundService,它在 ExecuteAsync 内部有一个阻塞调用,但令人惊讶的是(对我来说)它实际上并没有阻塞我的应用程序,我想知道原因.

根据我能找到的BackgroundService源代码的不同版本,Task ExecuteAsync方法在StartAync内部被火速调用了。来源如下。

        public virtual Task StartAsync(CancellationToken cancellationToken)
        {
            // Store the task we're executing
            _executingTask = ExecuteAsync(_stoppingCts.Token);

            // If the task is completed then return it, this will bubble cancellation and failure to the caller
            if (_executingTask.IsCompleted)
            {
                return _executingTask;
            }

            // Otherwise it's running
            return Task.CompletedTask;
        }

据我了解,await 调用的延续(即它下面的任何内容)将在任务返回后调用此异步方法的同一 SynchronizationContext 中执行。如果这是真的,那么为什么这个延续代码(具有阻塞调用)不会阻塞我的应用程序?

举例说明:

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            await Task.Yield();

            Foo(); // Foo blocks its executing thread until an I/O operation completes.
        }

由于永远不会等待 ExecutedAsync,因此该方法的继续(即 Foo 调用)将在最初触发 ExecuteAsync 任务的同一同步上下文中执行(它将在主如果我理解正确,请线程)。

我怀疑 Asp.Net 运行时必须有自己的SynchronizationContext,它实际上在不同的线程中执行异步延续或类似的东西。

任何人都可以在这里解释一下吗?

【问题讨论】:

    标签: c# asp.net-core async-await


    【解决方案1】:

    据我所知...我建议你先阅读this

    现在,我假设你得到的 asp.net 核心本身实际上已经采用了无上下文的方法。

    另外,当你构建主机时,通常由CreateHostBuilder(args).Build().Run();实际运行主机,它是IHost接口的一个实例,而不是IHostedServiceBackgroundService抽象类继承自。

    简而言之,任何从BackgroundService 基类继承的东西都可以被认为是一个特殊的对象,由宿主自己管理和执行,而不是在与宿主自己使用的线程相同的级别上运行。

    您可以在后台服务上放置一个阻塞线程的操作,但这是处理您的服务的线程,而不是应用程序线程本身。

    更新

    由于@underthevoid 希望进一步检查我所说的“与主机本身使用的线程不同的级别”,请允许我在这里澄清一些观点。

    首先,CreateHostBuilder(args).Build().Run(); 构建一个WebHost 的实例,并通过调用.Run() 来执行它,它在屏幕后面调用Start() 方法,你可以看到它here

    然后,_hostedServiceExecutor = _applicationServices.GetRequiredService<HostedServiceExecutor>(); 这条线发生了奇迹,之后它会立即调用 StartAsync(cancellationToken)

    现在,查看the HostedServiceExecutor,在构造函数中,它会获取您在应用程序中注册的所有IHostedService 实例,然后执行它们,正如您在代码中简要看到的那样。

    现在每个IHostedService 都是一个执行自己的Tasks 的对象,与处理WebHost 的Task 无关。现在它们都分开了。

    我的意思是different level as the thread that got use by the host itself,我试图指出两件事。

    • 您使用的所有BackgroundService 都被WebHost 调用,没有它,它们将无能为力。
    • 它们在单独的任务上运行,因此是单独的线程(我不认为一个线程可以像实现代码的方式那样同时处理多个任务)。

    关于任务是如何执行的,一起来看看@Stephen Cleary 的回答,相信他胜过你自己,我也有同样的信念。

    顺便说一句...this 也是一些关于BackgroundService 的好情侣,我也从他们那里得到了很多东西。

    希望这会有所帮助!

    【讨论】:

    • 谢谢!您是否有任何参考文本来解释 BackgroundService 是如何在“与主机本身使用的线程不同的级别”中处理的?
    • @underthevoid 很难找到满足您问题的文档,因此我通过 asp.net 核心本身的来源进行了解释,但评论中完全不适合。在Update 部分再次查看答案。希望对您有所帮助
    • 非常感谢您的更新!然而,我仍然不清楚我是否应该明确地创建一个长时间运行 任务来执行我对Foo 的阻塞调用,或者这是否已经由HostedServiceExecutor 完成。你对此有什么见解吗?我主要担心的是我在一个非常长的 I/O 操作期间阻塞了一个线程池线程,这不应该发生;我想我应该在一个单独的线程中执行它,而不是线程池线程,我错了吗?
    • @underthevoid 实际上,BackgroundService 是为解决您的确切问题而生的,不用担心我的朋友,因为它在 MS 自己的 official document 上。对了推荐部分的第二点!
    • 还有一点......线程池是许多可用线程的集合,由应用程序本身管理,它不仅仅是一个线程调用线程池线程,所以保留一个或几个线程来处理你很长的任务是绝对可以接受的,除非你有几十个……
    【解决方案2】:

    await 调用的继续(即它下面的任何内容)将在调用此异步方法的同一个 SynchronizationContext 中执行

    仅当存在 SynchronizationContext 时。然后就看具体的Context了,并不是所有的SynchronizationContexts都有Thread affinity。

    我认为在 asp.net 中 SynchronizationContext.Current 为空,我确信在同一线程上等待后它不会继续。

    【讨论】:

      【解决方案3】:

      有两点起作用:SynchronizationContext 和 TaskScheduler。

      在 Asp.Core 中,默认的 SyncContext 为空,允许每个任务在任何可用线程上运行。其次,TaskScheduler 开始发挥作用。如果它在同一个线程上运行多个任务,则可以阻塞所有其他任务。

      但是作为后台服务运行的任务被标记为LongRunning给任务调度器,并且任务调度器给每个长时间运行的任务它自己的线程。因此,您的后台服务无法通过阻塞线程调用来阻塞 asp 核心引擎。

      【讨论】:

      • 谢谢!您是否有任何参考文本来解释如何将BackgroundService 设置为LongRunning 任务?此外,如果BackgroundService 被设置为长时间运行的任务(我假设这是对StartAsync 返回的任务完成的),那么如果我忘记了Task.Yield() 顶部的Task.Yield() 行,为什么我的应用程序会冻结@987654327 @?
      【解决方案4】:

      据我了解,await 调用的继续(即它下面的任何内容)将在任务返回后调用此异步方法的同一个 SynchronizationContext 中执行。

      A bit of clarification:当await异步操作时,它捕获当时的当前“上下文”(而不是方法调用开始时的上下文是什么)。这个“上下文”是SynchronizationContext.Current,除非它是null,在这种情况下“上下文”是TaskScheduler.Current

      请注意,TaskScheduler.Current 绝不是 null。如果没有自定义TaskScheduler,则TaskScheduler.CurrentTaskScheduler.Default 相同,后者是一个将工作调度到.NET 线程池的任务调度程序。更简单的说,如果没有自定义的SynchronizationContext,也没有自定义的TaskScheduler,那么continuation 就会排队到线程池中。

      ...相同的同步上下文...(如果我理解正确,它将在主线程中运行)。

      Synchronization contexts don't have a 1:1 relationship with threads。确实,对于 GUI 应用程序,UI 同步上下文会将工作排队到特定的 UI 线程。但在 ASP.NET Classic(pre-Core)中,它的同步上下文会将工作排队到线程池。即使在 UI 世界中,同一个 UI 线程也可能有多个不同的同步上下文(例如,多窗口 WPF 应用程序曾经这样做,现在仍然使用 AFAIK)。

      我怀疑 Asp.Net 运行时必须有自己的 SynchronizationContext,它实际上在不同线程中执行异步延续或类似的东西。

      ASP.NET Core has no SynchronizationContext at all,因此应用默认行为:继续排队到线程池。

      【讨论】:

      • 所以我在BackgroundService.ExecuteAsync 中对Foo 的调用实际上是阻塞了一个线程池线程?如果是这样,我不应该开始一个新的长时间运行任务来代替Foo调用吗?
      • @underthevoid:可能不会。如果“长时间运行”是指LongRunning 标志,则通常使用Task.Run 比使用StartNew 更好。或者您可以使用隐式线程池线程保持原样。 LongRunning 仅应在性能测试表明有必要时使用。
      • 完美,非常感谢您的澄清!
      猜你喜欢
      • 1970-01-01
      • 2016-06-26
      • 1970-01-01
      • 1970-01-01
      • 2020-08-06
      • 2017-04-14
      • 2021-11-12
      • 2020-01-02
      • 1970-01-01
      相关资源
      最近更新 更多