.Net Core的`IHostedService`应该启动一个新线程吗?

7

.NET Core中BackgroundService或者IHostedService的启动方法是异步的:

//IHostedService
Task StartAsync(CancellationToken cancellationToken);
//BackgroundService
Task ExecuteAsync(CancellationToken stoppingToken);

那么我应该在ExecuteAsync/StartAsync 方法中编写所有逻辑,还是应该立即启动新线程并立即返回?
例如,以下两种实现方式哪种是正确的?
1.
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    new Thread(async () => await DoWork(stoppingToken)).Start();

    await Task.CompletedTask;
}

private async Task DoWork(CancellationToken stoppingToken) 
{
    while (!stoppingToken.IsCancellationRequested)
        //actual works
}

2.

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        //actual works
        await Task.Delay(1000);//e.g
    }
}

从语义上讲,第二个似乎是正确的,但如果有多个IHostedService,它们能够并行运行吗?

编辑1

我还编写了一个示例程序,说明托管服务并不是作为独立线程运行。

在输入q之前,控制台不会显示消息"Waiting for signal.."

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace BackgroundTaskTest
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var host = new HostBuilder()
                .ConfigureServices((hostContext, services) =>
                {
                    IConfiguration config = hostContext.Configuration;

                    //register tasks
                    services.AddHostedService<ReadService>();
                    services.AddHostedService<BlockService>();
                })
                .UseConsoleLifetime()
                .Build();

            await host.RunAsync();
        }
    }

    public static class WaitClass
    {
        public static AutoResetEvent Event = new AutoResetEvent(false);
    }

    public class ReadService : BackgroundService
    {
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                var c = Console.ReadKey();
                if (c.KeyChar == 'q')
                {
                    Console.WriteLine("\nTrigger event");
                    WaitClass.Event.Set();
                }
                await Task.Delay(1);
            }
        }
    }

    public class BlockService : BackgroundService
    {
        protected override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                Console.WriteLine("Waiting for signal..");
                WaitClass.Event.WaitOne();
                Console.WriteLine("Signal waited");
            }
            return Task.CompletedTask;
        }
    }
}

1
托管服务已经在它们自己的线程中运行。开启新线程没有任何意义。 - Chris Pratt
2
@ChrisPratt 我曾经认为托管服务已经在单独的线程中运行。但我发现,如果我注册的第一个服务阻塞了执行,那么接下来的服务就不会被执行... - mosakashaka
1
从我的测试来看,我认为当第一个await操作被触发时,托管服务会进入一个独立的线程。这是异步操作的行为,它可能会将这些异步操作放到独立的线程中,但我认为不能保证await会将操作放到独立的线程中... - mosakashaka
@ChrisPratt 如果我理解正确,所有 await 后面的代码都是 continuation,它们可能会在另一个线程中运行,也可能不会。从我的测试代码来看,所有主机服务都在同一个线程中运行(因为前面的服务将阻塞后面的服务),只有使用 await Task.Delay(xxx) 后面的服务才会运行。延迟超时后,前面的服务 可能 与后面的服务并行运行,因为前面的服务是 continuation,可能在单独的线程中运行。但正如你所提到的,这并不是保证的,所以我仍然需要自己创建线程... - mosakashaka
2
@ChrisPratt,托管服务是独立的,但它们不在分离的线程中。你必须创建自己的线程来使它们在独立的线程中运行。 - mosakashaka
显示剩余3条评论
2个回答

10

只需按照第二个示例所示,使用 async/await 实现即可,不需要额外的 Thread

顺便提一下:只有在 COM 互操作时才应该使用 Thread;对于 Thread 的每一个其他用例,现今都有更好的解决方案。一旦输入 new Thread,您就已经有了遗留代码。


谢谢。托管服务是否已在单独的线程中运行?我编写了注册多个服务的代码,发现如果我的第一个服务阻塞执行(没有Task.Delay),那么接下来的服务将不会运行(没有来自后续服务的打印)。 - mosakashaka
还有其他实现线程函数的方法吗?您是不是意味着如果我正在编写一个“线程”,我应该总是将其更改为“HostedService”?请详细解释一下。 - mosakashaka
1
这个回答完全误解了async/await和线程。仅仅拥有async和await并不能保证你的代码会异步运行,也不能说Thread是过时的代码。现在更常见的做法是使用Task.Run代替以前使用的新线程,但它并不允许访问底层线程的所有功能。 - Craig Miller
2
@CraigMiller:我喜欢你的评论!“async”并不一定意味着异步;因为op在他们的工作示例中使用了“await Task.Delay”,所以我假设他们有实际的异步工作要做。如果实现是同步的,我仍然建议使用“Task.Run”。我仍然相信,在除COM互操作之外的所有情况下,“Thread”都已经过时了;我在调试时确实会错过线程名称,但仅此而已。有比“Thread”更现代且更易于处理的解决方案,适用于除COM互操作之外的所有情况。 - Stephen Cleary
4
@CraigMiller,在回复这个人之前,我建议你先查一下他是谁,不要说这个人不理解异步/等待和线程。 - julealgon
显示剩余3条评论

3

对于 StartAsync() 方法,如果你的任务需要较长时间才能完成,至少应该启动一个单独的线程。否则,在我的经验中,使用 .NET Core 2.1 在 Windows 10 上,IHostedService 将不会在所有工作完成之前注册为“已启动”。

我有一个测试控制台应用程序,HTTP-GETs 相同的 URL 360 次,每次等待 10 秒,计算它成功了多少次以及失败了多少次。在这个应用程序中,我配置了 ILogger 日志记录器,使用 NLog,同时写入文本文件和控制台,最高日志详细级别。

一旦托管应用程序完成启动,我会看到一条消息记录在控制台(和文件中),告诉我托管应用程序已经启动,并且我可以按 Ctrl+C 键退出。

如果在 StartAsync() 中,我只是简单地 await 所有工作的测试方法,那么我直到所有工作都完成后一小时后,才会看到那条消息被记录。相反,如果我像你的第一个示例一样启动新线程,那么我几乎立即看到 Ctrl+C 的说明,正如我应该看到的那样。

因此,根据经验证明,你的第一个示例是正确的。


StartAsync 也会为您执行 hostbuilder.Build() 命令。可能根据此 StartAsync,您的日志条目位置不正确?也就是说,主任务是否记录了应用程序已启动,或者您的日志是在托管服务内部编写的?甚至是其他地方?在我的情况下,我将关于启动应用程序的日志移动到了创建 hostbuilder 时的 configureservices 中(我的日志配置和 DI 注册托管服务)。我不会说这是最佳实践,但嘿,我正在学习 ;) - Michael
1
@Michael, 这个带有 Ctrl+C 指令的日志条目并不是来自我的代码。据我所知,它来自 .NET Core 基础设施。 - Stephen G Tuggy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接