.net core 非阻塞的异步编程 及 线程调度过程
本文主要分为三个部分:
1、语法格式
2、线程调度情况
3、编程注意事项
4、练一练
* 阅读提示 :鼠标悬停在 章节标题 上可见 文章目录
异步编程(Task Asynchronous Programming,TAP),一种编程模式(Task-based Asynchronous Pattern)。
TAP 是 .NET 中推荐的异步编程模式,基于 Task 和 Task<TResult> 类型,用于表示异步。
异步编程一般应对两种场景,一是 I/O 绑定,当需要网络连接(连接数据库或读写到文件系统等)等耗时长的任务;二是 CPU 绑定,需要耗时长的计算。
1、简单的语法格式
.net 一直在为开发人员简化和标准化异步的写法,从 .net 4.5 开始就已经支持使用 aysnc 和 await 的关键字。
异步的语法格式如下:
private async Task<TResult> DoSomeStuffAsync(..)
{
..
await ..
..
}
l 关键词 async 本身不具备什么意义,只是装饰,当方法冠以 async 关键词,方法体内允许使用 await
l await 是标记需要等待的地方,但其本质并非阻塞线程。
l “非阻止操作”:指当运行到 await 时,会把当前线程返回到上一级调用者继续执行,如果没有上一级调用者,则该线程当场释放。
|
非阻止操作 |
阻止操作 |
备注 |
|
| 获取任务返回 | await task | task.Wait / task.Result |
非阻塞:线程遇到 await 时会返回上一层调用者继续执行,如果没有上一级调用者,则释放该线程; 阻塞:线程在等待期间不能执行其他任务,也不释放线程,硬等 |
|
任一任务完成 |
await Task.WhenAny |
Task.WaitAny |
|
|
所有任务完成 |
await Task.WhenAll |
Task.WaitAll |
|
|
等待一段时间 |
await Task.Delay |
Thread.Sleep |
l 异步方法返回的值总是 Task 的实例,可以是 Task 类型或 Task<TResult>,其中 TResult 是执行的方法的返回类型
l 如果直接拿异步方法的结果,形如 var obj = GetSomethingAsync(),这个 obj 是一个 Task 对象,其中 obj.AsyncStatus,obj.Result 可见执行情况
l await + 执行异步方法,形如 await GetSomethingAsync() 会得到 TResult 实例
l 直接使用 task.Result 得到的任务结果其状态是未知的,应该使用 await task,保证任务是 Completed 的
l 一般地,异步方法的名字需要添加后缀 Async,以便于写代码的时候区分开同步方法和异步方法
l 等待异步任务的执行过程中,如果其中发生了错误,该异步任务的外层 try catch 会捕捉到,它也是一种任务结果
2、异步运行机制,线程调度
观察以下代码,思考一下控制台会输出什么?
public static async Task Main(string[] args)
{
logMessage("Main <<----");
var task = GetUrlContentLengthAsync();
logMessage("Main ---->>");
await task;
logMessage("ALL COMPLETED");
}
static async Task<int> GetUrlContentLengthAsync()
{
logMessage("GetUrlContentLengthAsync start ");
using var client = new HttpClient();
Task<string> getStringTask = client.GetStringAsync("https://learn.microsoft.com/dotnet");
// Do some independent work..
var contents = await getStringTask;
logMessage("GetUrlContentLengthAsync end ");
return contents.Length;
}
static void logMessage(string msg)
{
Console.WriteLine($"{DateTime.Now.ToString("MM-dd HH:mm:ss.fff")} [{Thread.CurrentThread.ManagedThreadId}] {msg}");
}
打印结果:
11-18 18:34:34.563 [1] Main <<----
11-18 18:34:34.632 [1] GetUrlContentLengthAsync start
11-18 18:34:34.810 [1] Main ---->>
11-18 18:34:37.283 [7] GetUrlContentLengthAsync end
11-18 18:34:37.286 [7] ALL COMPLETED
先分析一下 GetUrlContentLengthAsync 这个异步方法,简单归纳会存在以下步骤:
- httpClient.GetStringAsync 发起 HTTP 请求,并立即返回一个未完成的任务。
- await 关键字会暂停 GetStringAsync 方法的执行,并将控制权返回给调用方。
- 任务调度器通过操作系统的通知机制来监听 HTTP 请求的响应。
- 操作系统在后台监控 I/O 操作的状态,并在操作完成时通知应用程序。
- 任务调度器随后选择一个可用的线程来继续执行 异步方法的剩余部分(await 之后)。
线程运行过程如下:

* HTTP 请求是一个 I/O 操作,操作系统会通过 I/O 完成端口(IOCP)来处理这些操作。
* IOCP 是一种高效的机制,用于处理异步 I/O 操作。它允许操作系统在 I/O 操作完成时通知应用程序。
3、注意事项
l 如果调用了 异步方法,一定要 await 任务执行结果
反面示例:
public static async Task Main(string[] args)
{
logMessage("Main <<----");
// fault example: if do not wait the result then we will do not know what happened on it
GetSomeStuff();
logMessage("Main ---->>");
}
这个异步任务已经在执行,但是却没有后续处理,不知道是成功或是失败,又或者一直在执行没有办法停止,这是危险的。
l 避免使用 task.Result,应该使用 await
| 使用 await,而避免使用 xxTask.Result 阻塞线程:如果任务尚未完成,访问 Result 会阻塞当前线程,直到任务完成。这会导致性能问题,特别是在 UI 线程中使用时,会导致界面卡顿。 死锁风险:在某些情况下,特别是在同步上下文(如 UI 线程)中,访问 Result 可能会导致死锁(任务等待当前线程释放,而当前线程又在等待任务完成)。 异常处理:直接访问 Result 可能会忽略任务中的异常。使用 await 可以更好地处理异常。 |
l 是否线程安全
异步编程的机制,允许到正在处理一个请求时,同时存在多个线程在处理操作,如果在对同一个对象做写入操作,这是危险的。
所以并行时的任务最好是没有关系的。
如果使用锁,需要考虑是否会导致死锁的问题。
l 异步编程可能会增加代码复杂度,慎重取舍
异步编程并不一定带来性能提升,毕竟上下文切换也是有开销的,对于简单的任务可能一条线做完的方式更合适。
4、练一练
以下代码有什么问题?
public async Task<ResultResponse> ValidateReceiptAsync(List<Guid> customerIds, ReceiptRequest request, CancellationToken cancellationToken)
{
var tokenTask = _medicalCheckServiceHelper.GetServiceToken(cancellationToken);
var medicalProvidersTask = _medicalProviderRepo.GetMedicalProvidersNames();
var customersTask = _customerService.GetByIdsAsync(customerIds);
var checkResults = await Task.WhenAll(request.UuidList.Select(uuid => _medicalCheckServiceHelper.GetResultAsync(uuid, tokenTask.Result, cancellationToken)));
return await ConsolidateReceiptsResult(medicalProvidersTask.Result, checkResults, await customersTask);
}
思考一下
- 1. tokenTask.Result 在任务完成之前访问会导致阻塞,应该在 await tokenTask 之后再访问。
- 2. 确认哪些可以并行处理,注意线程安全
这里有 4 个任务,分别是 获取 token(假作 taskA)、获取治疗厂商(taskB)、获取用户信息(taskC)、获取检查结果(taskD)
其中,taskD 依赖于 taskA ;现在需要确定 taskA、taskB、taskC 之间的依赖关系。
- a. 假设真实场景如下:
- i. taskA 取自于配置中心,也即它会走 HTTP 请求
- ii. taskD 是一个第三方接口,也即一个 HTTP 请求
- iii. taskB 和 taskC 取自同一个数据库,并且它们共用了一个数据库连接的上下文
- a. 假设真实场景如下:
那么可能的修改是这样的:
public async Task<ResultResponse> ValidateReceiptAsync(List<string> customerIds, ReceiptRequest request, CancellationToken cancellationToken)
{
var tokenTask = _medicalCheckServiceHelper.GetServiceToken(cancellationToken);
var medicalProvidersTask = _medicalProviderRepo.GetMedicalProvidersNames();
var token = await tokenTask;
var checkResults = await Task.WhenAll(request.UuidList.Select(uuid => _medicalCheckServiceHelper.GetResultAsync(uuid, token, cancellationToken)));
var medicalProviders = await medicalProvidersTask;
var customers = await _customerService.GetByIdsAsync(customerIds);
return await ConsolidateReceiptsResult(medicalProviders, checkResults, customers);
}
- b. 假设真实场景如下:
- i. taskA 取自于配置中心,也即它会走 HTTP 请求
- ii. taskD 是一个第三方接口,也即一个 HTTP 请求
- iii. taskB 和 taskC 取自不同数据库或不同的 HTTP 请求,它们相互独立
- b. 假设真实场景如下:
那么可能的修改是这样的:
public async Task<ResultResponse> ValidateReceiptAsync(List<string> customerIds, ReceiptRequest request, CancellationToken cancellationToken)
{
var tokenTask = _medicalCheckServiceHelper.GetServiceToken(cancellationToken);
var medicalProvidersTask = _medicalProviderRepo.GetMedicalProvidersNames();
var customersTask = _customerService.GetByIdsAsync(customerIds);
var token = await tokenTask;
var checkResultsTask = Task.WhenAll(request.UuidList.Select(uuid => _medicalCheckServiceHelper.GetResultAsync(uuid, token, cancellationToken)));
var medicalProviders = await medicalProvidersTask;
var customers = await customersTask;
var checkResults = await checkResultsTask;
return await ConsolidateReceiptsResult(medicalProviders, checkResults, customers);
}
但其实并不尽完善,因为这种写法的可读性并没有那么好,看起来更追求资源优化。
所以说引入了 异步编程 的话,代码是会变复杂的,每写一步都需要慎重考虑。
Reference:
[1] The Task Asynchronous Programming (TAP) model with async and await" - C# | Microsoft Learn
[2] Asynchronous programming scenarios - C# | Microsoft Learn
[3] Asynchronous programming - C# | Microsoft Learn
.net core 非阻塞的异步编程 及 线程调度过程的更多相关文章
- GIL全局解释器锁,线程池与进程池 同步异步,阻塞与非阻塞,异步回调
GIL全局解释器锁 1.什么是GIL 官方解释:'''In CPython, the global interpreter lock, or GIL, is a mutex that prevents ...
- 网络IPC:套接字之非阻塞和异步I/O
通常,recv函数没有数据可用时会阻塞等待.同样地,当套接字输出队列没有足够空间来发送消息时函数send会阻塞.在套接字非阻塞模式下,行为会改变.在这种情况下,这些函数不会阻塞而是失败,设置errno ...
- nio 阻塞 非阻塞 同步 异步
https://mp.weixin.qq.com/s/5SKgdkC0kaHN495psLd3Tg 说在前面 上篇NIO相关基础篇二,主要介绍了文件锁.以及比较关键的Selector,本篇继续NIO相 ...
- 阻塞IO,非阻塞IO,异步IO和非异步IO 的区别
最近在研究java IO.NIO.NIO2(或者称AIO)相关的东西,有些概念还是要明确下. 按照<Unix网络编程>的划分,IO模型可以分为:阻塞IO.非阻塞IO.IO复用.信号驱动IO ...
- 阻塞 , 非阻塞 , 同步 ,异步 , I/O模型
•阻塞,非阻塞:进程/线程要访问的数据是否就绪,进程/线程是否需要等待: •同步,异步:访问数据的方式,同步需要主动读写数据,在读写数据的过程中还是会阻塞:异步只需要I/O操作完成的通知,并不主动读写 ...
- java的高并发IO原理,阻塞BIO同步非阻塞NIO,异步非阻塞AIO
原文地址: IO读写的基础原理 大家知道,用户程序进行IO的读写,依赖于底层的IO读写,基本上会用到底层的read&write两大系统调用.在不同的操作系统中,IO读写的系统调用的名称可能不完 ...
- [INet] I/O模型:同步阻塞,同步非阻塞,异步非阻塞
POSIX 把这同步.异步两个术语定义 如下: 同步 I/O 操作( synchronous I/O opetation) 导致请求进程阻塞, 直到 I/O 操作完成: 异步 I/O 操作( asyn ...
- 进程理论 阻塞非阻塞 同步异步 I/O操作
1.什么是进程 进程指的是一个正在运行的程序,进程是用来描述程序执行过程的虚拟概念 进程的概念起源于操作系统,进程是操作系统最核心的概念,操作系统其它所有的概念都是围绕进程来的 2.操作系统 操作系统 ...
- 阻塞IO 非阻塞IO 异步IO
阻塞IO 一般表现为 进程/线程 调用IO操作后就一直死循环等待,直至IO操作结束,返回IO结果 非阻塞IO 一般表现为 进程/线程 调用IO操作后,可以先去干别的事情,但是每隔一段时间,回去询问一下 ...
- 非阻塞方式connect编程
参考博客: ①setsockopt()函数使用详解:http://blog.csdn.net/tody_guo/article/details/5972588 ②setsockopt :SO_LING ...
随机推荐
- 【Spring】Sring基础概念(黑马SSM学习笔记)
目录 Spring简介 Spring是什么 Spring发展 Spring优势 Spring体系结构 Spring快速入门 Spring程序开发步骤 不用Spring的一般步骤 使用Spring框架 ...
- Qt音视频开发40-人脸识别离线版
一.前言 上一篇文章写了在线调用人脸识别api进行处理,其实很多的客户需求是要求离线使用的,尤其是一些事业单位,严禁这些刷脸数据外泄上传到服务器,尽管各个厂家号称严格保密这些数据,但要阻止这些担心,唯 ...
- Qt编写的项目作品30-录音播放控件(雨田哥作品)
一.功能特点 使用FMOD音频引擎开发,支持跨平台,虚拟频道,插件设计. 数字回放,多个声卡,多路输出,多路输入. 自定义回放延迟,网络特性. 支持类型:DLS.M3U.ASX.WAX.PLS.AIF ...
- [转]Linux系统下的GCC编译过程、使用命令详解(多文件编译、动态库、静态库)
Linux系统下的GCC编译过程.使用命令详解(多文件编译.动态库.静态库) [Linux]gcc简介+编译过程 翻译 搜索 复制
- 基于开源IM即时通讯框架MobileIMSDK:RainbowChat v8.3版已发布
关于MobileIMSDK MobileIMSDK 是一套专门为移动端开发的开源IM即时通讯框架,超轻量级.高度提炼,一套API优雅支持UDP .TCP .WebSocket 三种协议,支持iOS.A ...
- 《计算机体系结构与SoC设计》(二)
1. 多指令流单数据流 多指令流单数据流(Multiple Instruction Stream, Single Data Stream,简称 MISD)是一种处理器设计概念,它允许处理器在单个时钟周 ...
- Redis学习笔记之Jedis
Jedis语法总结 Jedis是Java代码操作Redis的工具包,里面封装了操作Redis的方法 Jedis jedis = new Jedis(String ip , String port) 1 ...
- 探索自联接(SELF JOIN):揭示数据间复杂关系的强大工具
title: 探索自联接(SELF JOIN):揭示数据间复杂关系的强大工具 date: 2025/1/11 updated: 2025/1/11 author: cmdragon excerpt: ...
- 单点认证(SSO)方案调研总结
SSO方案 SSO介绍 单点登录(SSO)是一种身份验证解决方案,可让用户通过一次性用户身份验证登录多个应用程序和网站.这意味着用户只需输入一次用户名和密码,即可访问所有相互信任的系统,而无需在每个系 ...
- java基础知识回顾之java Socket学习
UDP传输:面向无连接的协议,不可靠,只是把应用程序传给IP层的数据报包发送出去,不保证发送出去的数据报包能到达目的地.不用再客户端和服务器端建立连接,没有超时重发等机制,传输速度快是它的优点.就像寄 ...