精:.net core 非阻塞的异步编程 及 线程调度过程

科技   2024-11-21 07:16   北京  

本文主要分为三个部分:

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 tasktask.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 这个异步方法,简单归纳会存在以下步骤:

  1. httpClient.GetStringAsync 发起 HTTP 请求,并立即返回一个未完成的任务。

  2. await 关键字会暂停 GetStringAsync 方法的执行,并将控制权返回给调用方。

  3. 任务调度器通过操作系统的通知机制来监听 HTTP 请求的响应。

  4. 操作系统在后台监控 I/O 操作的状态,并在操作完成时通知应用程序。

  5. 任务调度器随后选择一个可用的线程来继续执行 异步方法的剩余部分(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 

直接使用 task.Result 的方式拿结果是线程阻塞的方式,也即如果任务还没完成,那么这个线程将什么都不干,只等待任务完成。

使用 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 之间的依赖关系。

 


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

 

    • 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(https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/task-asynchronous-programming-model)

[2] Asynchronous programming scenarios - C# | Microsoft Learn(https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/async-scenarios)

[3] Asynchronous programming - C# | Microsoft Learn(https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/)


 Carcar019

出处:cnblogs.com/carmen-019/p/18555115


关注公众号DotNet开发跳槽    


DotNet开发跳槽
本公众号专注为.net开发工程师提供一个学习技术及求职/跳槽的交流平台。不定期分享NET技术类文章、面试题、求助技巧等干货,原创文章300+篇,让.net开发工程师学习/面试不再迷茫。ps: 后台回复“跳槽”,免费领取.NET开发面试题!
 最新文章