.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 中,现已推出。
相关文章:
.NET 8 中引入新的 IHostedLifecycleService 接口 实现定时任务
在这篇文章中,我们将了解 .NET 8 中为托管服务引入的一些新生命周期事件。请注意,这篇文章与 .NET 8 相关,在撰写本文时,.NET 8 目前处于预览状态。在 11 月 .NET 8 最终版本发布之前,类型和实现可能会发生变化。要继续…...

Redis--Geo指令的语法和使用场景举例(附近的人功能)
文章目录 前言Geo介绍Geo指令使用使用场景:附近的人参考文献 前言 Redis除了常见的五种数据类型之外,其实还有一些少见的数据结构,如Geo,HyperLogLog等。虽然它们少见,但是作用却不容小觑。本文将介绍Geo指令的语法和…...
127.0.0.1和0.0.0.0的区别
在网络开发中,经常会涉及到两个特殊的IP地址:127.0.0.1和0.0.0.0。这两者之间有一些关键的区别,本文将深入介绍它们的作用和用途。 127.0.0.1 127.0.0.1 是本地回环地址,通常称为 “localhost”。作用是让网络应用程序能够与本地…...
SpringBoot ES 聚合后多字段加减乘除
SpringBoot ES 聚合后多字段加减乘除 在SpringData Elasticsearch中,聚合统计的原理主要依赖于Elasticsearch本身的聚合框架。Elasticsearch提供了强大的聚合功能,使得你可以对文档进行各种计算和统计,从而得到有关数据集的有用信息。 Elast…...
React16源码: React中requestCurrentTime和expirationTime的源码实现补充
requestCurrentTime 1 )概述 关于 currentTime,在计算 expirationTime 和其他的一些地方都会用到 从它的名义上来讲,应等于performance.now() 或者 Date.now() 就是指定的当前时间在react整体设计当中,它是有一些特定的用处和一些…...

【论文阅读】Deep Graph Contrastive Representation Learning
目录 0、基本信息1、研究动机2、创新点3、方法论3.1、整体框架及算法流程3.2、Corruption函数的具体实现3.2.1、删除边(RE)3.2.2、特征掩盖(MF) 3.3、[编码器](https://blog.csdn.net/qq_44426403/article/details/135443921)的设…...

设计模式-简单工厂
设计模式-简单工厂 简单工厂模式是一个集中管理对象创建,并根据条件生成所需类型对象的设计模式,有助于提高代码的复用性和维护性,但可能会导致工厂类过于复杂且违反开闭原则。 抽象提取理论: 封装对象创建过程解耦客户端与产品…...
Django ORM 中的单表查询 API(1)
在 Django 中,对象关系映射(ORM)提供了一种功能强大、表现力丰富的数据库交互方式。ORM 允许开发人员使用高级 Python 代码执行数据库查询,从而更轻松地处理数据库实体。 下面,我们将探讨 Django ORM 中单表查询 API …...

电子雨html代码
废话不多说下面是代码: <!DOCTYPE html><html lang"en"><head><meta charset"UTF-8"><title>Code</title><style>body{margin: 0;overflow: hidden;}</style></head><body><c…...
xadmin基于Django的后台管理系统安装与使用
xadmin是基于Django的后台管理系统 官网:http://sshwsfc.github.io/xadmin/ github地址:https://github.com/sshwsfc/xadmin 安装方式 pip安装 pip install xadmin在setting配置中添加: INSTALLED_APPS [xadmin,crispy_forms, ]在urls.py…...

[go语言]输入输出
目录 知识结构 输入 1.Scan 编辑 2.Scanf 3.Scanln 4.os.Stdin --标准输入,从键盘输入 输出 1.Print 2.Printf 3.Println 知识结构 输入 为了展示集中输入的区别,将直接进行代码演示。 三者区别的结论:Scanf格式化输入&#x…...
【SpringBoot系列】AOP详解
🤵♂️ 个人主页:@香菜的个人主页,加 ischongxin ,备注csdn ✍🏻作者简介:csdn 认证博客专家,游戏开发领域优质创作者,华为云享专家,2021年度华为云年度十佳博主 🐋 希望大家多多支持,我们一起进步!😄 如果文章对你有帮助的话, 欢迎评论 💬点赞👍🏻 收…...
openssl3.2 - 官方demo学习 - signature - rsa_pss_hash.c
文章目录 openssl3.2 - 官方demo学习 - signature - rsa_pss_hash.c概述笔记END openssl3.2 - 官方demo学习 - signature - rsa_pss_hash.c 概述 对私钥对明文做签名(摘要算法为SHA256) 用公钥对密文做验签(摘要算法为SHA256) 笔记 /*! \file rsa_pss_hash.c \note openss…...
Redis相关知识点
1.什么是Redis Redis (REmote DIctionary Server) 是用 C 语言开发的一个开源的高性能键值对(key-value)数据库,它支持网络,可基于内存亦可持久化,并提供多种语言的API。Redis具有高效性、原子性、支持多种数据结构、…...

嵌入式开发--STM32G4系列片上FLASH的读写
这个玩意吧,说起来很简单,就是几行代码的事,但楞是折腾了我大半天时间才搞定。原因后面说,先看代码吧: 读操作 读操作很简单,以32位方式读取的时候是这样的: data *(__IO uint32_t *)(0x080…...

嵌入式-Stm32-江科大基于标准库的GPIO的八种模式
文章目录 一:GPIO输入输出原理二:GPIO基本结构三:GPIO位结构四:GPIO的八种模式道友:相信别人,更要一百倍地相信自己。 (推荐先看文章:《 嵌入式-32单片机-GPIO推挽输出和开漏输出》…...
2024年1月17日Arxiv热门NLP大模型论文:THE FAISS LIBRARY
Meta革新搜索技术!提出Faiss库引领向量数据库性能飞跃 引言:向量数据库的兴起与发展 随着人工智能应用的迅速增长,需要存储和索引的嵌入向量(embeddings)数量也在急剧增加。嵌入向量是由神经网络生成的向量表示&…...

深度解析JVM类加载器与双亲委派模型
概述 Java虚拟机(JVM)是Java程序运行的核心,其中类加载器和双亲委派模型是JVM的重要组成部分。本文将深入讨论这两个概念,并解释它们在实际开发中的应用。 1. 什么是类加载器? 类加载器是JVM的一部分,负…...

前端下载文件流,设置返回值类型responseType:‘blob‘无效的问题
前言: 本是一个非常简单的请求,即是下载文件。通常的做法如下: 1.前端通过Vue Axios向后端请求,同时在请求中设置响应体为Blob格式。 2.后端相应前端的请求,同时返回Blob格式的文件给到前端(如果没有步骤…...

C++核心编程——类和对象(一)
本专栏记录C学习过程包括C基础以及数据结构和算法,其中第一部分计划时间一个月,主要跟着黑马视频教程,学习路线如下,不定时更新,欢迎关注。 当前章节处于: ---------第1阶段-C基础入门 ---------第2阶段实战…...

eNSP-Cloud(实现本地电脑与eNSP内设备之间通信)
说明: 想象一下,你正在用eNSP搭建一个虚拟的网络世界,里面有虚拟的路由器、交换机、电脑(PC)等等。这些设备都在你的电脑里面“运行”,它们之间可以互相通信,就像一个封闭的小王国。 但是&#…...

Unity3D中Gfx.WaitForPresent优化方案
前言 在Unity中,Gfx.WaitForPresent占用CPU过高通常表示主线程在等待GPU完成渲染(即CPU被阻塞),这表明存在GPU瓶颈或垂直同步/帧率设置问题。以下是系统的优化方案: 对惹,这里有一个游戏开发交流小组&…...

Appium+python自动化(十六)- ADB命令
简介 Android 调试桥(adb)是多种用途的工具,该工具可以帮助你你管理设备或模拟器 的状态。 adb ( Android Debug Bridge)是一个通用命令行工具,其允许您与模拟器实例或连接的 Android 设备进行通信。它可为各种设备操作提供便利,如安装和调试…...
MySQL 隔离级别:脏读、幻读及不可重复读的原理与示例
一、MySQL 隔离级别 MySQL 提供了四种隔离级别,用于控制事务之间的并发访问以及数据的可见性,不同隔离级别对脏读、幻读、不可重复读这几种并发数据问题有着不同的处理方式,具体如下: 隔离级别脏读不可重复读幻读性能特点及锁机制读未提交(READ UNCOMMITTED)允许出现允许…...

相机Camera日志实例分析之二:相机Camx【专业模式开启直方图拍照】单帧流程日志详解
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了: 这一篇我们开始讲: 目录 一、场景操作步骤 二、日志基础关键字分级如下 三、场景日志如下: 一、场景操作步骤 操作步…...

遍历 Map 类型集合的方法汇总
1 方法一 先用方法 keySet() 获取集合中的所有键。再通过 gey(key) 方法用对应键获取值 import java.util.HashMap; import java.util.Set;public class Test {public static void main(String[] args) {HashMap hashMap new HashMap();hashMap.put("语文",99);has…...

centos 7 部署awstats 网站访问检测
一、基础环境准备(两种安装方式都要做) bash # 安装必要依赖 yum install -y httpd perl mod_perl perl-Time-HiRes perl-DateTime systemctl enable httpd # 设置 Apache 开机自启 systemctl start httpd # 启动 Apache二、安装 AWStats࿰…...
【Web 进阶篇】优雅的接口设计:统一响应、全局异常处理与参数校验
系列回顾: 在上一篇中,我们成功地为应用集成了数据库,并使用 Spring Data JPA 实现了基本的 CRUD API。我们的应用现在能“记忆”数据了!但是,如果你仔细审视那些 API,会发现它们还很“粗糙”:有…...

【Zephyr 系列 10】实战项目:打造一个蓝牙传感器终端 + 网关系统(完整架构与全栈实现)
🧠关键词:Zephyr、BLE、终端、网关、广播、连接、传感器、数据采集、低功耗、系统集成 📌目标读者:希望基于 Zephyr 构建 BLE 系统架构、实现终端与网关协作、具备产品交付能力的开发者 📊篇幅字数:约 5200 字 ✨ 项目总览 在物联网实际项目中,**“终端 + 网关”**是…...
鱼香ros docker配置镜像报错:https://registry-1.docker.io/v2/
使用鱼香ros一件安装docker时的https://registry-1.docker.io/v2/问题 一键安装指令 wget http://fishros.com/install -O fishros && . fishros出现问题:docker pull 失败 网络不同,需要使用镜像源 按照如下步骤操作 sudo vi /etc/docker/dae…...