.NET 8 中引入新的 IHostedLifecycleService 接口 实现定时任务

        在这篇文章中,我们将了解 .NET 8 中为托管服务引入的一些新生命周期事件。请注意,这篇文章与 .NET 8 相关,在撰写本文时,.NET 8 目前处于预览状态。在 11 月 .NET 8 最终版本发布之前,类型和实现可能会发生变化。要继续操作,您将需要.NET 8:Announcing .NET 8 - .NET Blog。

 IHostedLifecycleService 简介

        主要更改是在 Microsoft.Extensions.Hosting 命名空间中包含一个名为 IHostedLifecycleService 的新接口。此接口继承自现有的 IHostedService 接口,对其进行扩展以添加在现有 StartAsync 和 StopAsync 方法之前或之后发生的新生命周期事件的方法。这些提供了一种方法来挂钩某些高级场景的更具体的应用程序生命周期事件。

接口定义如下:

public partial interface IHostedLifecycleService : Microsoft.Extensions.Hosting.IHostedService
{Task StartingAsync(CancellationToken cancellationToken);Task StartedAsync(CancellationToken cancellationToken);Task StoppingAsync(CancellationToken cancellationToken);Task StoppedAsync(CancellationToken cancellationToken);    
}

        实现此接口的所有已注册托管服务的 StartAsync 方法将在应用程序生命周期的早期运行,然后在任何已注册托管服务上调用 StartAsync(来自 IHostedService)。这可用于在应用程序启动之前执行一些非常早期的验证检查,例如检查关键需求或可用的依赖项。这使得应用程序可能在任何托管服务开始执行其主要工作负载之前启动失败。其他用途包括“预热”和初始化单例以及应用程序使用的其他状态。

        注册托管服务的所有 StartAsync(来自 IHostedService)方法完成后,将在实现上调用 StartedAsync。这可用于在将应用程序标记为成功启动之前验证应用程序状态或条件。

        StoppingAsync 和 StoppedAsync 在应用程序关闭期间的工作方式类似,并为关闭前和关闭后验证提供高级挂钩。

        在讨论更详细的细节之前,值得讨论一下为什么 Microsoft 创建了一个新的派生接口,而不是利用默认接口实现来更新现有的 IHostedService 接口。对于默认接口实现来说,这确实是一个很好的例子,可以将它们添加到具有默认无操作实现的 IHostedService 中。当我们查看该库的运行时目标时,原因很明显。托管包多目标各种目标框架。这包括netstandard2.0,它在引入默认接口实现功能之前就被锁定了。因此,为了继续支持这个目标,改用衍生的接口设计。

        作为引入此接口的 PR 的一部分,HostOptions 中还添加了一个新选项 StartupTimeout。这允许提供一个 TimeSpan 来控制所有托管服务启动所允许的最长时间。当配置为非无限值(默认值)时,传递给启动生命周期事件的取消令牌将链接到使用提供的值配置的 CancellationTokenSource。

使用新界面
        使用 .NET 8 ,我们可以查看如何使用此新界面的一般示例。我在应用程序中看到的一项相当常见的启动工作是初始化数据库。

        在生产过程中,我们可以预期此类数据库是在线的、可用的和种子的;在其他环境中,例如 CI,我们可能需要创建一个虚拟数据库并使用示例数据进行播种。存在多种解决方案来处理此问题,但一种可能的选择是使用托管服务有条件地执行工作。当其他托管服务依赖于可用数据库时,这可能会更加复杂,因为这些服务必须在数据库准备就绪后以正确的顺序启动。在 .NET 7 中,这是可以实现的,因为托管服务按顺序并按照注册顺序启动。

        因此,在 .NET 7 中,我们可以实现以下目标:

public class ServiceA : IHostedService
{public Task StartAsync(CancellationToken cancellationToken){// INIT DBreturn Task.CompletedTask;}public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}public class ServiceB : BackgroundService
{protected override Task ExecuteAsync(CancellationToken stoppingToken){// USE DBreturn Task.CompletedTask;}
}

为了向 DI 容器注册这些服务以便主机执行它们,我们必须确保以正确的顺序专门添加它们。

builder.Services.AddHostedService<ServiceA>();

builder.Services.AddHostedService<ServiceB>();

        由于 .NET 7 按顺序而不是同时执行每个服务的 StartAsync 方法,因此我们知道,在调用 ServiceB.StartAsync 时,数据库应该已在 ServiceA 中完成初始化。

        虽然默认情况下在 .NET 8 中也是如此,但现在也可以配置主机以同时启动它们。如果我们想更改此选项,我们的应用程序可能会崩溃,因为 ServiceB 会与 ServiceA 同时触发。这可能不是一个重要问题,但如果应用程序中有其他托管服务,通过切换到并发执行,我们可以减少应用程序的整体启动时间。

        通过在 .NET 8 中引入新的 IHostedLifecycleService,我们可以将数据库初始化工作移至生命周期的早期,同时还可以利用并发托管服务启动。

public class ServiceA : IHostedService, IHostedLifecycleService
{public Task StartingAsync(CancellationToken cancellationToken){// INIT DBreturn Task.CompletedTask;}public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}public class ServiceB : BackgroundService
{protected override Task ExecuteAsync(CancellationToken stoppingToken){// USE DBreturn Task.CompletedTask;}
}

        在上面的示例代码中,我们定义了两个托管服务。除了 IHostedService 之外,ServiceA 还实现了新的 IHostedLifecycleService。我们希望在应用程序生命周期的早期、任何主要工作负载之前执行数据库初始化。因此,我们可以将数据库设置代码包含在 GettingAsync 方法中。

        派生自BackgroundService 的ServiceB 现在可以在其ExecuteAsync 方法内安全地使用数据库,因为ExecuteAsync 是由IHostedService 接口中定义的StartAsync 的底层实现调用的。因此,在注册服务的所有 StartingAsync 方法完成之前,不会调用它。

        我们将以与 .NET 中相同的方式向 DI 容器注册这些服务,但添加它们的顺序不再重要。

builder.Services.AddHostedService<ServiceB>();

builder.Services.AddHostedService<ServiceA>();

        即使按照这个顺序,ServiceA的StartingAsync也会在ServiceB.StartAsync之前执行。我们甚至可以配置并发启动和停止行为,而不会破坏我们的逻辑。

builder.Services.Configure<HostOptions>(options =>

{

    options.ServicesStartConcurrently = true;

    options.ServicesStopConcurrently = true;

});

深入细节

  引入新接口后,最核心的更改是在内部 Host 类内部实现的,该类实现了 IHost 接口。此类定义了主 Host,在从 ASP.NET Core 和 Worker Service 等模板创建新应用程序时构建。IHost 接口定义了当应用程序启动或停止时调用的 StartAsync 和 StopAsync 方法。

  第一个有意义的更改在 StartAsync 方法的开头引入了额外的逻辑,以实现新的StartupTimeout 功能。

CancellationTokenSource? cts = null;
CancellationTokenSource linkedCts;
if (_options.StartupTimeout != Timeout.InfiniteTimeSpan)
{cts = new CancellationTokenSource(_options.StartupTimeout);linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken, _applicationLifetime.ApplicationStopping);
}
else
{linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _applicationLifetime.ApplicationStopping);
}

        从更新后的代码中我们可以看到,在所有情况下,传递到 StartAsync 的取消令牌都可能导致取消,就像 IHostApplicationLifetime 上公开的 ApplicationStopping 令牌一样,如果触发关闭,该令牌将被标记为已取消。

        当 HostOptions.StartupTimeout 不等于 InfiniteTimeSpan 时,将创建 CancellationTokenSource,并将 TimeSpan 传递到构造函数中。然后可以将其令牌添加到链接的令牌源,以确保第三个条件也可以触发中止启动。这个新选项允许应用程序开发人员为预期的“正常”启动提供预期的上限时间,这样在特殊情况下,长时间的延迟就可以主动触发启动的中止。这种情况可能是由不可用的外部依赖项引起的,应进行跟踪和记录,以便进行调查。

        创建 linkedCts 后,它将用于访问 CancellationToken,然后将其传递到启动过程中调用的任何后续异步方法中。

CancellationToken token = linkedCts.Token;

        第一个异步方法是 IHostLifetime.WaitForStartAsync 的调用,这是托管生命周期中的早期挂钩,此时正在等待。这是另一个高级挂钩,可能会延迟启动,直到收到外部事件信号为止。这个概念从 .NET Core 3.0 开始就已经存在。

// This may not catch exceptions.

await _hostLifetime.WaitForStartAsync(token).ConfigureAwait(false);        

token.ThrowIfCancellationRequested();

实现中的下一行准备一些变量和字段。

List<Exception> exceptions = new();

_hostedServices = Services.GetRequiredService<IEnumerable<IHostedService>>();

_hostedLifecycleServices = GetHostLifecycles(_hostedServices);

bool concurrent = _options.ServicesStartConcurrently;

bool abortOnFirstException = !concurrent;

        设置列表以包含启动期间的任何异常后,将从容器中检索所有已注册的 IHostedService。GetHostLifecycles 方法用于循环 IHostedService 实现并确定哪个(如果有)也实现 IHostedLifecycleService。

下一段代码为每个 IHostedLifecycleService 执行 GettingAsync 方法。

if (_hostedLifecycleServices is not null)
{// Call StartingAsync().await ForeachService(_hostedLifecycleServices, token, concurrent, abortOnFirstException, exceptions,(service, token) => service.StartingAsync(token)).ConfigureAwait(false);
}

        ForeachService 是一个辅助方法,它根据作为参数传入的 HostOptions 设置并发或顺序执行服务。

private static async Task ForeachService<T>(IEnumerable<T> services,CancellationToken token,bool concurrent,bool abortOnFirstException,List<Exception> exceptions,Func<T, CancellationToken, Task> operation)
{if (concurrent){// The beginning synchronous portions of the implementations are run serially in registration order for// performance since it is common to return Task.Completed as a noop.// Any subsequent asynchronous portions are grouped together run concurrently.List<Task>? tasks = null;foreach (T service in services){Task task;try{task = operation(service, token);}catch (Exception ex){exceptions.Add(ex); // Log exception from sync method.continue;}if (task.IsCompleted){if (task.Exception is not null){exceptions.AddRange(task.Exception.InnerExceptions); // Log exception from async method.}}else{tasks ??= new();tasks.Add(Task.Run(() => task, token));}}if (tasks is not null){Task groupedTasks = Task.WhenAll(tasks);try{await groupedTasks.ConfigureAwait(false);}catch (Exception ex){exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable());}}}else{foreach (T service in services){try{await operation(service, token).ConfigureAwait(false);}catch (Exception ex){exceptions.Add(ex);if (abortOnFirstException){return;}}}}
}

        代码基于布尔并发参数进行分支。让我们重点关注用于同时调用每个服务的功能的代码。

        正如注释所述,实现首先在每个服务上调用操作委托,在本例中为“StartingAsync”。许多 IHostedLifecycleService 完全有可能通过返回缓存的 Task.CompletedTask 在其大多数方法上实现无操作。在这些情况下,代码同步运行,因为没有什么可等待的。上面的代码对此进行了特殊处理,并检查是否有任何任务立即返回完成或抛出同步异常。对于这些已完成的任务,其中引发的任何异常都会添加到异常列表中。

        对于此时尚未完成的任何任务,它们都是异步运行的。这些任务将添加到任务列表中。所有任务启动后,就会使用 WhenAll 等待它们,这意味着它们会同时运行,直到所有注册的服务都完成其工作。任何异常也会在这里捕获。

        在非并发路径中,代码更简单,因为它可以简单地按顺序等待每个操作。在此配置中,每个服务必须在调用下一个服务之前完成其工作。

        返回 Host.StartAsync 方法,对 IHostedService.StartAsync 和 IHostedLifecycleService.StartedAsync 重复该过程。该方法最后会记录并重新抛出任何捕获的异常,然后触发托管应用程序现已启动的通知。

        Stopping 和 Stopped 的新生命周期事件的实现几乎相同,因此我们无需在这里深入讨论。

概括
        Microsoft 在每个版本中不断完善和增强 .NET 中的托管概念。在 .NET 8 中,重点放在引入对托管服务的启动行为的更多控制。在这项最新的工作中,他们引入了对启动和关闭之前、期间和之后运行的代码的高级、细粒度控制。新的 IHostedLifecycleService 接口出现在.NET 8 中,现已推出。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/241881.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

css3+javaScript实现一个左右钟摆-摇晃的红灯笼网页特效

css3javaScript实现一个左右钟摆-摇晃的红灯笼网页特效&#xff01;前天逛博客时无意中看见了&#xff0c;别人的博客顶部有一个会左右钟摆的摇晃的红灯笼&#xff0c;产生了想法&#xff0c;我也想给自己做一个&#xff0c;但是网上找了很多方案&#xff0c;都没有实现。终于在…

tidb Cloud 连接spring boot 项目

一、 免费试用tidbitcloud TiDB Cloud Documentation | PingCAP Docs 1.github账号登录 2.创建集群 3.点击对应集群cludter0 导入数据 导入 本地导入只支持csv文件&#xff0c;其他导入需要AWZ账号使用S3云存储 二、连接spingboot项目 选择java&#xff0c;复制下面的jd…

spring boot学习第八篇:通过spring boot、jedis实现秒单

参考&#xff1a;Redis实现分布式锁的7种方案 - 知乎 1、 准备数据库表&#xff0c;如下SQL表示库存表&#xff0c;有主键ID和库存数量字段 CREATE TABLE t_stock (id bigint(20) NOT NULL AUTO_INCREMENT,quantity bigint(20) NOT NULL,PRIMARY KEY (id) ) ENGINEInnoDB DEF…

Transformer详解(附代码实现及翻译任务实现)

一&#xff1a;了解背景和动机 阅读Transformer论文&#xff1a; 阅读原始的Transformer论文&#xff1a;“Attention is All You Need”&#xff0c;由Vaswani等人于2017年提出&#xff0c;是Transformer模型的开创性工作。 二&#xff1a;理解基本构建块 注意力机制&#…

源码:Spring常规Bean创建过程

Bean创建过程&#xff1a; 一、版本 5.3.10二、学习内容 Bean创建过程源码三、Bean生命周期 时间轴地址&#xff1a;点击 四、bean创建过程脑图总结 脑图地址&#xff1a;点击 五、源码过程 说明&#xff1a; bean创建入口一般都是通过getBean(xxx);方法进入的&#xf…

C 语言->编译和链接实现原理

✅作者简介&#xff1a;大家好&#xff0c;我是橘橙黄又青&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;橘橙黄又青-CSDN博客 今天学习&#xff1a;浅学编译和链接内部实现原理 前提&#xff1a;本文是在gcc编译环…

「Kafka」Broker篇

「Kafka」Broker篇 主要讲解的是在 Kafka 中是怎么存储数据的&#xff0c;以及 Kafka 和 Zookeeper 之间如何进行数据沟通的。 Kafka Broker 总体工作流程 Zookeeper 存储的 Kafka 信息 启动 Zookeeper 客户端&#xff1a; [atguiguhadoop102 zookeeper-3.5.7]$ bin/zkCli.sh通…

git 删除 submodule 子模块的步骤

实验有效&#xff0c;这里删除了两个 submodule。 1, 执行删除 submodule mkdir tmp1 && cd tmp1 && git clone --recursive ssh://gitaaa.bbb.ccc.git \ && cd ccc/ && git checkout -b abranch_01 \ && git submodule deinit -f…

如何给新华网投稿发稿?新华网的媒体发稿方法步骤

现如今&#xff0c;互联网已经成为了人们获取信息的主要途径&#xff0c;各大媒体网站也成为了发布自己作品的首选平台。其中&#xff0c;新华网作为中国最具影响力的新闻媒体之一&#xff0c;其内容覆盖面广、触及人群众多&#xff0c;因此&#xff0c;能够在新华网上发表文章…

ASEPRITE使用笔记

aseprite学习笔记 快捷键 新建图层后,按快捷键c可以调出画布属性框放大缩小画布快捷键,鼠标滚轮移动画布快捷键,空格ctr+d,取消选取基本概念 软件五个基本区域:菜单栏、工具属性栏、工具栏、图层栏、颜色栏颜色栏分为色板和调色区域注意事项 创造时,需要把输入法调整成应…

Docker:6种网络配置详解浅介

在Docker中&#xff0c;网络配置是一个重要的主题&#xff0c;因为容器需要与其他容器或外部网络进行通信。Docker提供了多种网络模式和配置选项&#xff0c;以便在不同的场景下满足用户的需求。 本文介绍这些网络模式的区别以及配置&#xff0c;相信看完以后你能够掌握Docker网…

【AI】ChatGPT和文心一言那个更好用

大家好&#xff0c;我是全栈小5&#xff0c;欢迎阅读文章&#xff01; 此篇是【话题达人】序列文章&#xff0c;这一次的话题是《自然语言处理的发展》 文章将以博主的角度进行讲述&#xff0c;理解和水平有限&#xff0c;不足之处&#xff0c;望指正。 目录 背景自我介绍面试题…

在线扒站网PHP源码-在线扒站工具网站源码

源码介绍 这是一款在线的网站模板下载程序&#xff0c;也就是我们常说的扒站工具&#xff0c;利用它我们可以很轻松的将别人的网站模板样式下载下来&#xff0c;这样就可以大大提高我们编写前端的速度了&#xff01;注&#xff1a;扒取的任何站点不得用于商业、违法用途&#…

day3:基于UDP模型的简单文件下载

思维导图 tftp文件下载客户端实现 #include <head.h> #define SER_PORT 69 #define SER_IP "192.168.125.223" int link_file() {int sfdsocket(AF_INET,SOCK_DGRAM,0);if(sfd-1){perror("socket error");return -1;}return sfd; } int filedownloa…

关于java的继承

关于java的继承 我们在上一篇文章中&#xff0c;了解到了封装&#xff0c;我们本篇文章来介绍一下面向对象的第二大特点&#xff0c;继承&#xff0c;还是遵循结合现实生活中的实际情况&#xff0c;理解着去学习&#xff0c;能更好的加深印象&#x1f600;。 一、继承 继承的…

逻辑运算

目录 AND OR NOT Oracle从入门到总裁:https://blog.csdn.net/weixin_67859959/article/details/135209645 逻辑运算可以保证连接多个条件&#xff0c;连接主要使用 AND、OR 、NOT完成 AND 1.查询职位不是办事员&#xff0c;但是工资低于 300 的员工信息 这个范例可以理…

Windows系统字体尺寸学习

调用GetTextMetrics来获得字体尺寸信息, 函数返回设备描述表中当前选定的字体信息&#xff1b; 返回值到TEXTMETRIC类型的结构中&#xff1b; 返回字段值的单位取决于当前设备描述表映射方式&#xff1b;默认映射方式是MM_TEXT&#xff0c;值的单位是像素&#xff1b; 前7个字…

脱离于ASP.NET 和Visual Studio编辑Razor脚本

Razor Pad是一个编辑Razor脚本的工具&#xff0c;脱离于ASP.NET 和Visual Studio。 github地址&#xff1a;GitHub - RazorPad/RazorPad: RazorPad is a quick and simple stand-alone editing environment that allows anyone (even non-developers) to author Razor templat…

Python项目——搞怪小程序(PySide6+Pyinstaller)

1、介绍 使用python编写一个小程序&#xff0c;回答你是猪吗。 点击“是”提交&#xff0c;弹窗并退出。 点击“不是”提交&#xff0c;等待5秒&#xff0c;重新选择。 并且隐藏了关闭按钮。 2、实现 新建一个项目。 2.1、设计UI 使用Qt designer设计一个UI界面&#xff0c…

Three.JS教程1 环境搭建、场景与相机

Three.JS教程1 环境搭建、场景与相机 一、Three.JS简介二、环境搭建1. 开发准备2. 安装 three.js3. 新建文件index.htmlmain.js 4. 关于附加组件5. 启动 三、创建场景1. 场景的概念2. 相机的概念3. 相机的几个相关概念&#xff08;1&#xff09;视点&#xff08;Position&#…