c# - 同时启动多个长时间运行的后台服务

标签 c# .net-core background-process asp.net-core-hosted-services

我正在 dotnet core 2.2 中试验 IHostedService。我的任务是创建 2 个后台长时间运行的任务。

  • 第一个是管理 Selenium 浏览器 session (打开/关闭选项卡、解析 DOM)并将电子邮件放入 ConcurrentBag 内的队列中。
  • 第二个后台工作人员每 10 分钟发送一次电子邮件通知,针对 ConcurrentBag(第一个任务添加的)中存在的消息。它还将它们分组在一起,以便仅发送 1 条消息。

但是,我在同时运行 2 个托管进程时遇到问题。似乎只有第一个托管进程正在执行,而第二个进程则等待第一个进程完全执行。但因为我从没想过它会完成 - 第二个过程永远不会开始......

我是否滥用了IHostedService?如果是,那么完成我的任务的最佳架构方法是什么?

这是我当前正在使用的代码(试图完成):

using System;
// ..

namespace WebPageMonitor
{
    class Program
    {
        public static ConcurrentBag<string> Messages = new ConcurrentBag<string>();

        static void Main(string[] args)
        {
            BuildWebHost(args)
                .Run();

            Console.ReadKey();
        }

        private static IHost BuildWebHost(string[] args)
        {
            var hostBuilder = new HostBuilder()
                .ConfigureHostConfiguration(config =>
                {
                    config.AddJsonFile("emailSettings.json", optional: true);
                    config.AddEnvironmentVariables();
                })
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddOptions();

                    var bindConfig = new EmailSettings();
                    hostContext.Configuration.GetSection("EmailSettings").Bind(bindConfig);
                    services.AddSingleton<EmailSettings>(bindConfig);

                    services.AddTransient<EmailSender>();

                    services.AddHostedService<BrowserWorkerHostedService>();
                    services.AddHostedService<EmailWorkerHostedService>();
                });

            return hostBuilder.Build();
        }

    }
}

BrowserWorkerHostedService

public class BrowserWorkerHostedService : BackgroundService
{
    private static IWebDriver _driver;

    public BrowserWorkerHostedService()
    {
        InitializeDriver();
    }

    private void InitializeDriver()
    {
        try
        {
            ChromeOptions options = new ChromeOptions();
            options.AddArgument("start-maximized");
            options.AddArgument("--disable-infobars");
            options.AddArgument("no-sandbox");

            _driver = new ChromeDriver(options);
        }
        catch (Exception ex)
        {
            Program.Messages.Add("Exception: " + ex.ToString());

            Console.WriteLine($" Exception:{ex.ToString()}");
            throw ex;
        }
    }

    protected override async Task ExecuteAsync(CancellationToken stopToken)
    {
        while (!stopToken.IsCancellationRequested)
        {
            try
            {
                _driver.Navigate().GoToUrl("https://www.google.com");
                Program.Messages.Add("Successfully opened a website!");
                // rest of the processing here

                Thread.Sleep(60_000);
            }
            catch (Exception ex)
            {
                Program.Messages.Add("Exception: " + ex.ToString());

                Console.WriteLine(ex.ToString());
                Thread.Sleep(120_000);
            }
        }

        _driver?.Quit();
        _driver?.Dispose();
    }
}

EmailWorkerHostedService

public class EmailWorkerHostedService : BackgroundService
{
    private readonly EmailSender _emailSender;
    private readonly IHostingEnvironment _env;

    public EmailWorkerHostedService(
        EmailSender emailSender,
        IHostingEnvironment env)
    {
        _emailSender = emailSender;
        _env = env;
    }

    protected override async Task ExecuteAsync(CancellationToken stopToken)
    {
        while (!stopToken.IsCancellationRequested)
        {
            var builder = new StringBuilder();

            List<string> exceptionMessages = new List<string>();
            string exceptionMessage;
            while (Program.Messages.TryTake(out exceptionMessage))
                exceptionMessages.Add(exceptionMessage);

            if (exceptionMessages.Any())
            {
                foreach (var message in exceptionMessages)
                {
                    builder.AppendLine(new string(message.Take(200).ToArray()));
                    builder.AppendLine();
                }

                string messageToSend = builder.ToString();
                await _emailSender.SendEmailAsync(messageToSend);
            }

            Thread.Sleep(10000);
        }
    }
}

编辑:应用答案中建议的更改后,这是有效的代码的当前版本。添加 await 有帮助。

最佳答案

首先,切勿在异步上下文中使用Thread.Sleep(),因为它会阻塞操作。请改用Task.Delay()。我相信这就是你的问题。看BackgroundService.StartAsync实现:

    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;
    }

当实际调用 asyc 方法时,它同步执行,直到第一个真正的异步操作。你真正的异步操作是

await _emailSender.SendEmailAsync(messageToSend);

但只有满足条件才会调用

if (exceptionMessages.Any())

这意味着您的 ExecuteAsync 方法永远不会返回,因此 StartAsyncTask.Delay 也是真正的异步方法(Thread.Sleep 不是),因此在点击它之后 StartAsync 将继续并退出,然后你的第二个方法服务将有机会启动。

关于c# - 同时启动多个长时间运行的后台服务,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56916355/

相关文章:

c# - 使用反射获取方法名和参数

c# - 更改控制台应用程序图标(Visual Studio代码)

c# - MonoTouch.Dialog:响应 RadioGroup 选择

c# - 我如何在 C# 中使用 WkHtmlToXSharp

visual-studio-code - 如何使Visual Studio Code自动加载所需的 Assets

windows-8 - 在编写后台任务之前如何声明后台任务?

ruby-on-rails - 有没有办法让 Resque 平均分配给定的工作?

python - 很好地终止后台python脚本

c# - 如何在软键盘Unity3d中检测 "done"按钮

.Net 核心和插件