.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阶段实战…...
DockerHub与私有镜像仓库在容器化中的应用与管理
哈喽,大家好,我是左手python! Docker Hub的应用与管理 Docker Hub的基本概念与使用方法 Docker Hub是Docker官方提供的一个公共镜像仓库,用户可以在其中找到各种操作系统、软件和应用的镜像。开发者可以通过Docker Hub轻松获取所…...
条件运算符
C中的三目运算符(也称条件运算符,英文:ternary operator)是一种简洁的条件选择语句,语法如下: 条件表达式 ? 表达式1 : 表达式2• 如果“条件表达式”为true,则整个表达式的结果为“表达式1”…...

P3 QT项目----记事本(3.8)
3.8 记事本项目总结 项目源码 1.main.cpp #include "widget.h" #include <QApplication> int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); } 2.widget.cpp #include "widget.h" #include &q…...
Linux云原生安全:零信任架构与机密计算
Linux云原生安全:零信任架构与机密计算 构建坚不可摧的云原生防御体系 引言:云原生安全的范式革命 随着云原生技术的普及,安全边界正在从传统的网络边界向工作负载内部转移。Gartner预测,到2025年,零信任架构将成为超…...

零基础在实践中学习网络安全-皮卡丘靶场(第九期-Unsafe Fileupload模块)(yakit方式)
本期内容并不是很难,相信大家会学的很愉快,当然对于有后端基础的朋友来说,本期内容更加容易了解,当然没有基础的也别担心,本期内容会详细解释有关内容 本期用到的软件:yakit(因为经过之前好多期…...
使用Matplotlib创建炫酷的3D散点图:数据可视化的新维度
文章目录 基础实现代码代码解析进阶技巧1. 自定义点的大小和颜色2. 添加图例和样式美化3. 真实数据应用示例实用技巧与注意事项完整示例(带样式)应用场景在数据科学和可视化领域,三维图形能为我们提供更丰富的数据洞察。本文将手把手教你如何使用Python的Matplotlib库创建引…...

20个超级好用的 CSS 动画库
分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码,而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库,可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画,可以包含在你的网页或应用项目中。 3.An…...
掌握 HTTP 请求:理解 cURL GET 语法
cURL 是一个强大的命令行工具,用于发送 HTTP 请求和与 Web 服务器交互。在 Web 开发和测试中,cURL 经常用于发送 GET 请求来获取服务器资源。本文将详细介绍 cURL GET 请求的语法和使用方法。 一、cURL 基本概念 cURL 是 "Client URL" 的缩写…...
微服务通信安全:深入解析mTLS的原理与实践
🔥「炎码工坊」技术弹药已装填! 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 一、引言:微服务时代的通信安全挑战 随着云原生和微服务架构的普及,服务间的通信安全成为系统设计的核心议题。传统的单体架构中&…...
DAY 26 函数专题1
函数定义与参数知识点回顾:1. 函数的定义2. 变量作用域:局部变量和全局变量3. 函数的参数类型:位置参数、默认参数、不定参数4. 传递参数的手段:关键词参数5 题目1:计算圆的面积 任务: 编写一…...