「C#.NET 拾遗补漏」16:几个常见的TAP异步操作
ztj100 2024-10-28 21:09 32 浏览 0 评论
在本系列上一篇文章「C#.NET 拾遗补漏」15:异步编程基础 中,我们讲到,现代应用程序广泛使用的是基于任务的异步编程模式(TAP),历史的 EAP 和 AMP 模式已经过时不推荐使用。今天继续总结一下 TAP 的异步操作,比如取消任务、报告进度、Task.Yield()、ConfigureAwait() 和并行操作等。
虽然实际 TAP 编程中很少使用到任务的状态,但它是很多 TAP 操作机理的基础,所以下面先从任务状态讲起。
1.任务状态
Task 类为异步操作提供了一个生命周期,这个周期由 TaskStatus 枚举表示,它有如下值:
public enum TaskStatus
{
Created = 0,
WaitingForActivation = 1,
WaitingToRun = 2,
Running = 3,
WaitingForChildrenToComplete = 4,
RanToCompletion = 5,
Canceled = 6,
Faulted = 7
}
其中 Canceled、Faulted 和 RanToCompletion 状态一起被认为是任务的最终状态。因此,如果任务处于最终状态,则其 IsCompleted 属性为 true 值。
1.1 手动控制任务启动
为了支持手动控制任务启动,并支持构造与调用的分离,Task 类提供了一个 Start 方法。由 Task 构造函数创建的任务被称为冷任务,因为它们的生命周期处于 Created 状态,只有该实例的 Start 方法被调用才会启动。
任务状态平时用的情况不多,一般我们在封装一个任务相关的方法时,可能会用到。比如下面这个例子,需要判断某任务满足一定条件才启动:
static void Main(string[] args)
{
MyTask t = new(() =>
{
// do something.
});
StartMyTask(t);
Console.ReadKey();
}
public static void StartMyTask(MyTask t)
{
if (t.Status == TaskStatus.Created && t.Counter>10)
{
t.Start();
}
else
{
// 这里模拟计数,直到 Counter>10 再执行 Start
while (t.Counter <= 10)
{
// Do something
t.Counter++;
}
t.Start();
}
}
public class MyTask : Task
{
public MyTask(Action action) : base(action)
{
}
public int Counter { get; set; }
}
同样,TaskStatus.Created 状态以外的状态,我们叫它热任务,热任务一定是被调用了 Start 方法激活过的。
1.2 确保任务已激活
注意,所有从 TAP 方法返回的任务都必须被激活,比如下面这样的代码:
MyTask task = new(() =>
{
Console.WriteLine("Do something.");
});
// 在其它地方调用
await task;
在 await 之前,任务没有执行 Task.Start 激活,await 时程序就会一直等待下去。所以如果一个 TAP 方法内部使用 Task 构造函数来实例化要返回的 Task,那么 TAP 方法必须在返回 Task 对象之前对其调用 Start。
2.任务取消
在 TAP 中,取消对于异步方法实现者和消费者来说都是可选的。如果一个操作允许取消,它就会暴露一个异步方法的重载,该方法接受一个取消令牌(CancellationToken 实例)。按照惯例,参数被命名为 cancellationToken。例如:
public Task ReadAsync(
byte [] buffer, int offset, int count,
CancellationToken cancellationToken)
异步操作会监控这个令牌是否有取消请求。如果收到取消请求,它可以选择取消操作,如下面的示例通过 while 来监控令牌的取消请求:
static void Main(string[] args)
{
CancellationTokenSource source = new();
CancellationToken token = source.Token;
var task = DoWork(token);
// 实际情况可能是在稍后的其它线程请求取消
Thread.Sleep(100);
source.Cancel();
Console.WriteLine(#34;取消后任务返回的状态:{task.Status}");
Console.ReadKey();
}
public static Task DoWork(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
// Do something.
Thread.Sleep(1000);
return Task.CompletedTask;
}
return Task.FromCanceled(cancellationToken);
}
如果取消请求导致工作提前结束,甚至还没有开始就收到请求取消,则 TAP 方法返回一个以 Canceled 状态结束的任务,它的 IsCompleted 属性为 true,且不会抛出异常。当任务在 Canceled 状态下完成时,任何在该任务注册的延续任务仍都会被调用和执行,除非指定了诸如 NotOnCanceled 这样的选项来选择不延续。
但是,如果在异步任务在工作时收到取消请求,异步操作也可以选择不立刻结束,而是等当前正在执行的工作完成后再结束,并返回 RanToCompletion 状态的任务;也可以终止当前工作并强制结束,根据实际业务情况和是否生产异常结果返回 Canceled 或 Faulted 状态。
对于不能被取消的业务方法,不要提供接受取消令牌的重载,这有助于向调用者表明目标方法是否可以取消。
3.进度报告
几乎所有异步操作都可以提供进度通知,这些通知通常用于用异步操作的进度信息更新用户界面。
在 TAP 中,进度是通过 IProgress<T> 接口来处理的,该接口作为一个参数传递给异步方法。下面是一个典型的的使用示例:
static void Main(string[] args)
{
var progress = new Progress<int>(n =>
{
Console.WriteLine(#34;当前进度:{n}%");
});
var task = DoWork(progress);
Console.ReadKey();
}
public static async Task DoWork(IProgress<int> progress)
{
for (int i = 1; i <= 100; i++)
{
await Task.Delay(100);
if (i % 10 == 0)
{
progress?.Report(i);
};
}
}
输出如下结果:
当前进度:10%
当前进度:20%
当前进度:30%
当前进度:40%
当前进度:50%
当前进度:60%
当前进度:70%
当前进度:80%
当前进度:90%
当前进度:100%
IProgress<T> 接口支持不同的进度实现,这是由消费代码决定的。例如,消费代码可能只关心最新的进度更新,或者希望缓冲所有更新,或者希望为每个更新调用一个操作,等等。所有这些选项都可以通过使用该接口来实现,并根据特定消费者的需求进行定制。例如,如果本文前面的 ReadAsync 方法能够以当前读取的字节数的形式报告进度,那么进度回调可以是一个 IProgress<long> 接口。
public Task ReadAsync(
byte[] buffer, int offset, int count,
IProgress<long> progress)
再如 FindFilesAsync 方法返回符合特定搜索模式的所有文件列表,进度回调可以提供工作完成的百分比和当前部分结果集,它可以用一个元组来提供这个信息。
public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
string pattern,
IProgress<Tuple<double, ReadOnlyCollection<List<FileInfo>>>> progress)
或使用 API 特有的数据类型:
public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
string pattern,
IProgress<FindFilesProgressInfo> progress)
如果 TAP 的实现提供了接受 IProgress<T> 参数的重载,它们必须允许参数为空,在这种情况下,不会报告进度。IProgress<T> 实例可以作为独立的对象,允许调用者决定如何以及在哪里处理这些进度信息。
4.Task.Yield 让步
我们先来看一段 Task.Yield() 的代码:
Task.Run(async () =>
{
for(int i=0; i<10; i++)
{
await Task.Yield();
...
}
});
这里的 Task.Yield() 其实什么也没干,它返回的是一个空任务。那 await 一个什么也没做的空任务有什么用呢?
我们知道,对计算机来说,任务调度是根据一定的优先策略来安排线程去执行的。如果任务太多,线程不够用,任务就会进入排队状态。而 Yield 的作用就是让出等待的位置,让后面排除的任务先行。它字面上的意思就是让步,当任务做出让步时,其它任务就可以尽快被分配线程去执行。举个现实生活中的例子,就像你在排队办理业务时,好不容易到你了,但你的事情并不急,自愿让出位置,让其他人先办理,自己假装临时有事到外面溜一圈什么事也没干又回来重新排队。默默地做了一次大善人。
Task.Yield() 方法就是在异步方法中引入一个让步点。当代码执行到让步点时,就会让出控制权,去线程池外面兜一圈什么事也没干再回来重新排队。
5. 定制异步任务后续操作
我们可以对异步任务执行完成的后续操作进行定制。常见的两个方法是 ConfigureAwait 和 ContinueWith。
5.1 ConfigureAwait
我们先来看一段 Windows Form 中的代码:
private void button1_Click(object sender, EventArgs e)
{
var content = CurlAsync().Result;
...
}
private async Task<string> CurlAsync()
{
using (var client = new HttpClient())
{
returnawait client.GetStringAsync("http://geekgist.com");
}
}
想必大家都知道 CurlAsync().Result 这句代码在 Windows Form 程序中会造成死锁。原因是 UI 主线程执行到这句代码时,就开始等待异步任务的结果,处于阻塞状态。而异步任务执行完后回来准备找 UI 线程继续执行后面的代码时,却发现 UI 线程一直处于“忙碌”的状态,没空搭理回来的异步任务。这就造成了你等我,我又在等你的尴尬局面。
当然,这种死锁的情况只会在 Winform 和早期的 ASP.NET WebForm 中才会发生,在 Console 和 Web API 应用中不会生产死锁。
解决办法很简单,作为异步方法调用者,我们只需改用 await 即可:
private async void button1_Click(object sender, EventArgs e)
{
var content = await CurlAsync();
...
}
在异步方法内部,我们也可以调用任务的 ConfigureAwait(false) 方法来解决这个问题。如:
private async Task<string> CurlAsync()
{
using (var client = new HttpClient())
{
returnawait client
.GetStringAsync("http://geekgist.com")
.ConfigureAwait(false);
}
}
虽然两种方法都可行,但如果作为异步方法提供者,比如封装一个通用库时,考虑到难免会有新手开发者会使用 CurlAsync().Result,为了提高通用库的容错性,我们就可能需要使用 ConfigureAwait 来做兼容。
ConfigureAwait(false) 的作用是告诉主线程,我要去远行了,你去做其它事情吧,不用等我。只要先确保一方不在一直等另一方,就能避免互相等待而造成死锁的情况。
5.2 ContinueWith
ContinueWith 方法很容易理解,就是字面上的意思。作用是在异步任务执行完成后,安排后续要执行的工作。示例代码:
private void Button1_Click(object sender, EventArgs e)
{
var backgroundScheduler = TaskScheduler.Default;
var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
Task.Factory
.StartNew(_ => DoBackgroundComputation(), backgroundScheduler)
.ContinueWith(_ => UpdateUI(), uiScheduler)
.ContinueWith(_ => DoAnotherBackgroundComputation(), backgroundScheduler)
.ContinueWith(_ => UpdateUIAgain(), uiScheduler);
}
如上,可以一直链式的写下去,任务会按照顺序执行,一个执行完再继续执行下一个。若其中一个任务返回的状态是 Canceled 时,后续的任务也将被取消。这个方法有好些个重载,在实际用到的时候再查看文档即可。
6.总结
本文内容都是相对比较基础的 TAP 异步操作知识点。C# 的 TAP 很强大,提供的 API 也很多,远不止本文讲的这些,都是围绕 Task 转的。关键是要理解好基础操作,才能灵活使用更高级的功能。希望本文对你有所帮助。
参考:
https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap
https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/implementing-the-task-based-asynchronous-pattern
https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/consuming-the-task-based-asynchronous-pattern
相关推荐
- sharding-jdbc实现`分库分表`与`读写分离`
-
一、前言本文将基于以下环境整合...
- 三分钟了解mysql中主键、外键、非空、唯一、默认约束是什么
-
在数据库中,数据表是数据库中最重要、最基本的操作对象,是数据存储的基本单位。数据表被定义为列的集合,数据在表中是按照行和列的格式来存储的。每一行代表一条唯一的记录,每一列代表记录中的一个域。...
- MySQL8行级锁_mysql如何加行级锁
-
MySQL8行级锁版本:8.0.34基本概念...
- mysql使用小技巧_mysql使用入门
-
1、MySQL中有许多很实用的函数,好好利用它们可以省去很多时间:group_concat()将取到的值用逗号连接,可以这么用:selectgroup_concat(distinctid)fr...
- MySQL/MariaDB中如何支持全部的Unicode?
-
永远不要在MySQL中使用utf8,并且始终使用utf8mb4。utf8mb4介绍MySQL/MariaDB中,utf8字符集并不是对Unicode的真正实现,即不是真正的UTF-8编码,因...
- 聊聊 MySQL Server 可执行注释,你懂了吗?
-
前言MySQLServer当前支持如下3种注释风格:...
- MySQL系列-源码编译安装(v5.7.34)
-
一、系统环境要求...
- MySQL的锁就锁住我啦!与腾讯大佬的技术交谈,是我小看它了
-
对酒当歌,人生几何!朝朝暮暮,唯有己脱。苦苦寻觅找工作之间,殊不知今日之事乃我心之痛,难道是我不配拥有工作嘛。自面试后他所谓的等待都过去一段时日,可惜在下京东上的小金库都要见低啦。每每想到不由心中一...
- MySQL字符问题_mysql中字符串的位置
-
中文写入乱码问题:我输入的中文编码是urf8的,建的库是urf8的,但是插入mysql总是乱码,一堆"???????????????????????"我用的是ibatis,终于找到原因了,我是这么解决...
- 深圳尚学堂:mysql基本sql语句大全(三)
-
数据开发-经典1.按姓氏笔画排序:Select*FromTableNameOrderByCustomerNameCollateChinese_PRC_Stroke_ci_as//从少...
- MySQL进行行级锁的?一会next-key锁,一会间隙锁,一会记录锁?
-
大家好,是不是很多人都对MySQL加行级锁的规则搞的迷迷糊糊,一会是next-key锁,一会是间隙锁,一会又是记录锁。坦白说,确实还挺复杂的,但是好在我找点了点规律,也知道如何如何用命令分析加...
- 一文讲清怎么利用Python Django实现Excel数据表的导入导出功能
-
摘要:Python作为一门简单易学且功能强大的编程语言,广受程序员、数据分析师和AI工程师的青睐。本文系统讲解了如何使用Python的Django框架结合openpyxl库实现Excel...
- 用DataX实现两个MySQL实例间的数据同步
-
DataXDataX使用Java实现。如果可以实现数据库实例之间准实时的...
- MySQL数据库知识_mysql数据库基础知识
-
MySQL是一种关系型数据库管理系统;那废话不多说,直接上自己以前学习整理文档:查看数据库命令:(1).查看存储过程状态:showprocedurestatus;(2).显示系统变量:show...
- 如何为MySQL中的JSON字段设置索引
-
背景MySQL在2015年中发布的5.7.8版本中首次引入了JSON数据类型。自此,它成了一种逃离严格列定义的方式,可以存储各种形状和大小的JSON文档,例如审计日志、配置信息、第三方数据包、用户自定...
你 发表评论:
欢迎- 一周热门
-
-
MySQL中这14个小玩意,让人眼前一亮!
-
旗舰机新标杆 OPPO Find X2系列正式发布 售价5499元起
-
【VueTorrent】一款吊炸天的qBittorrent主题,人人都可用
-
面试官:使用int类型做加减操作,是线程安全吗
-
C++编程知识:ToString()字符串转换你用正确了吗?
-
【Spring Boot】WebSocket 的 6 种集成方式
-
PyTorch 深度学习实战(26):多目标强化学习Multi-Objective RL
-
pytorch中的 scatter_()函数使用和详解
-
与 Java 17 相比,Java 21 究竟有多快?
-
基于TensorRT_LLM的大模型推理加速与OpenAI兼容服务优化
-
- 最近发表
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)
- vmware17pro最新密钥 (34)
- mysql单表最大数据量 (35)