02020206 .NET Core重难点知识06 CancellationToken、WhenAll、异步其它问题
02020206 .NET Core重难点知识06 CancellationToken、WhenAll、异步其它问题
1. CancellationToken(视频Part2-11)
1.1 CancellationToken引入
- 大家在写代码时,经常容易忽略CancellationToken,好像也不影响开发。
- 异步程序通常执行比较耗时的操作,这里就存在这个耗时任务提前终止的可能性。
- 比如下载文件,太慢了,就取消了任务。
- 有时需要提前终止任务,比如:请求超时、用户取消请求。
- 很多异步方法都有CancellationToken参数,用于获得提前终止执行的信号。
- 客户都把浏览器关了,人都离开了,当前任务还在运行,这样会浪费资源。
1.2 ConcellationToken介绍
ConcellationToken → 结构体。
None → 空
bool IsConcellationRequested → 是否取消
(*)Register(Action callback) → 注册取消监听
ThrowIfConcellationRequested() → 如果任务被取消,执行到这句话就抛出异常。
CallcellationTokenSource → 类,用来创建ConcellationToken对象,我们并不直接使用ConcellationToken创建对象。
CancelAfter() → 超时后发出取消信号
Cancel() → 发出取消信号
CancellationToken Token
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
为“下载一个网站n次”的方法增加取消功能,需要用到:
GetStringAsync + IsCancellationRequested、GetStringAsync + ThrowIfCancellationRequested()、带CancellatioonToken的GetAsync()分别实现。用于取消分别用超时、用户敲按键(不能awati)实现。
1.3 ConcellationToken示例
// @1 下载网站100次
using System;
using System.Net.Http;
using System.Threading.Tasks;
namespace Demo02
{
class Program
{
static async Task Main(string[] args)
{
await DownloadAsync01("https://www.baidu.com", 100);
Console.ReadLine();
}
static async Task DownloadAsync01(string url, int n)
{
using(HttpClient client = new HttpClient())
{
for (int i = 0; i < n; i++)
{
string html = await client.GetStringAsync(url);
Console.WriteLine($"{DateTime.Now}:{html}");
}
}
}
}
}
控制台输出:
...
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// @2 下载网站100次,如果3S没有下载完,请求被取消
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Demo02
{
class Program
{
static async Task Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource(); // @2.3 new一个CancellationTokenSource对象
cts.CancelAfter(300); // @2.4 如果3S没有结束,就提前终止请求
CancellationToken ctoken = cts.Token;
await DownloadAsync01("https://www.baidu.com", 100, ctoken);
Console.ReadLine();
}
static async Task DownloadAsync01(string url, int n, CancellationToken cancellationToken) // @2.1 传入CancellationToken参数
{
using (HttpClient client = new HttpClient())
{
for (int i = 0; i < n; i++)
{
string html = await client.GetStringAsync(url);
Console.WriteLine($"{DateTime.Now}:{html}");
if(cancellationToken.IsCancellationRequested) // @2.2 检测到请求被取消
{
Console.WriteLine("请求被取消");
break;
}
}
}
}
}
}
控制台输出:
2025/8/25 21:08:53:<!DOCTYPE html>
...
请求被取消
说明:在@2.2处,通过代码判断然后取消。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// @3 自动抛出异常来取消
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Demo02
{
class Program
{
static async Task Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(300);
CancellationToken ctoken = cts.Token;
await DownloadAsync02("https://www.baidu.com", 100, ctoken);
Console.ReadLine();
}
static async Task DownloadAsync02(string url, int n, CancellationToken cancellationToken)
{
using (HttpClient client = new HttpClient())
{
for (int i = 0; i < n; i++)
{
string html = await client.GetStringAsync(url);
Console.WriteLine($"{DateTime.Now}:{html}");
cancellationToken.ThrowIfCancellationRequested(); // @3.1 自动抛出异常
}
}
}
}
}
异常信息:System.OperationCanceledException:“The operation was canceled.”
说明:此时需要我们手动处理异常。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// @4 通过C#自带的GetAsync()方法来取消
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Demo02
{
class Program
{
static async Task Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(300);
CancellationToken ctoken = cts.Token;
await DownloadAsync03("https://www.baidu.com", 100, ctoken);
Console.ReadLine();
}
static async Task DownloadAsync03(string url, int n, CancellationToken cancellationToken)
{
using (HttpClient client = new HttpClient())
{
for (int i = 0; i < n; i++)
{
var resp = await client.GetAsync(url, cancellationToken); // @4.1 返回Task<HttpResponseMessage>类型
string html = await resp.Content.ReadAsStringAsync(); // @4.2 通过resp,获取HTML的内容
Console.WriteLine($"{DateTime.Now}:{html}");
}
}
}
}
}
异常信息:System.OperationCanceledException:“The operation was canceled.”
1.4 总结
- ASP.NET Core开发中,一般不需要自己处理CancellationToken、CancellationTokenSource这些,只要做到能转发CancellationToke就转发即可。
- ASP.NET Core会对用户请求中断进行处理。
1.5 示例
// 演示ASP.NET Core中的使用:写一个方法,Delay1000次,用Debug.WriteLine()输出,访问中跳转到其它网站。
老师用ASP.NET Core项目演示,从视频20:00开始,后续需要重新看一遍,先过。
- 本节视频,其它的都可以不记住,因为是在讲原理,结论只有如下一句话:
- 在写ASP.NET Core程序的时候,Action参数里面尽量声明一个CancellationToken,在调用异步方法的时候,能传CancellationToken的时候都给它传上就可以了。这样尽量避免用户把浏览器关了,或者用户去浏览其它网站了,服务器还傻乎乎的执行。
2. WhenAll(视频Part2-12)
2.1 Task类的重要方法
1. Task<Task> WhenAny(IEnumerable<Task> tasks)等,任何一个Task完成,Task就完成。
2. Task<TResult[]> WhenAll<TResult>(params Task<TResult>[] tasks)等,所有Task完成,Task才完成。用于等待多个任务执行结束,但是不在乎它们的执行顺序。
3. FromResult()创建普通数值的Task对象。
2.2 代码解读
Task<string> t1 = File.ReadAllTextAsync("e:/01.txt"); // @1 异步方法,但是没有用await
Task<string> t2 = File.ReadAllTextAsync("e:/02.txt"); // @2 异步方法,但是没有用await
Task<string> t3 = File.ReadAllTextAsync("e:/03.txt"); // @3 异步方法,但是没有用await
string[] results = await Task.WhenAll(t1, t2, t3); // @4 这里使用await
string s1 = results[0];
string s2 = results[1];
string s3 = results[2];
说明:
1. 在@1,@2,@3处都没用await,而是直接使用Task类型来获取它们的值。这样三个方法并不是一个执行完了再执行下一个,而是这三个都读了,但是没有读完程序就往下走了。
2. 在@4处,将t1,t2,t3这三个作为参数,传给Task.WhenAll(静态方法)。WhenAll返回一个返回值,然后用数组results来接受。
2.1 注意此处的await,这样就是等t1,t2,t3这三个都执行完成,把它们三个的返回值放到数组results里面。
2.2 然后分别打印三个的返回值。
2.3 案例
- 计算一个文件夹下,所有文本文件的字符个数汇总。
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace Demo02
{
class Program
{
static async Task Main(string[] args)
{
string[] files = Directory.GetFiles(@"e:\temp\a"); // 拿到文件夹下所有的文件
Task<int>[] countTasks = new Task<int>[files.Length];
for (int i = 0; i < files.Length; i++)
{
string fn = files[i];
Task<int> t = ReadCharCount(fn);
countTasks[i] = t;
}
int[] counts = await Task.WhenAll(countTasks);
int c = counts.Sum(); // 计算数组的和
Console.WriteLine(c);
Console.ReadLine();
}
static async Task<int> ReadCharCount(string fileName)
{
string s = await File.ReadAllTextAsync(fileName);
return s.Length;
}
}
}
2. 异步其它问题(不经常用)
2.1 接口中异步方法的问题
- async是提示编译器为异步方法中的await代码进行分段处理的,而一个异步方法是否修饰了async对于方法的调用者来讲没有区别。
- 因此对于接口中的方法或者抽象方法不能修饰为async。
- 接口只是声明了方法的返回值和参数,接口没有实现方法。
- 正是因为接口没有实现方法,所以接口中的方法不用写async,只有在接口实现类里面的方法根据需要看看是否需要加async。
interface ITest
{
Task<int> GetCharCount(string file); // 接口中声明了返回值类型和参数类型,此时编译可以通过。
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
interface ITest
{
async Task<int> GetCharCount(string file); // 编译时会报错
}
报错信息:只能在具有正文的方法中使用“async”修饰符。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
interface ITest
{
async Task<int> GetCharCount(string file); // 编译时会报错
}
class MyClass : ITest
{
public async Task<int> GetCharCount(string file) // 实现类实现接口,可以用async,至于是否加上async看需求。
{
...
}
}
2.2 异步与yield
- yield return不仅能简化数据的返回,而且可以让数据处理“流水线化“,提示性能。
static IEnumerable<string> Test()
{
yield return "hello";
yield return "yzk";
yield return "youzack";
}
说明:
1. yield return也是会被编译器编译为状态机的方式。yield return不是一个真正的方法,它也是编译为一个类,一个类里面有很多状态机的调用。
2. yield return和await反编译后比较类似。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
using System;
using System.Collections.Generic;
namespace Demo02
{
class Program
{
static void Main(string[] args)
{
foreach (var s in Test2())
{
Console.WriteLine(s);
}
Console.ReadLine();
}
static IEnumerable<string> Test1() // @1
{
List<string> list = new List<string>();
list.Add("hello");
list.Add("yzk");
list.Add("youzack.com");
return list;
}
static IEnumerable<string> Test2() // @2 反编译后会发现yield会被拆分为3段
{
yield return "hello";
yield return "yzk";
yield return "youzack.com";
}
}
}
控制台输出:
hello
yzk
youzack.com
说明:
1. 在@1处,需要所有数据准备好了之后,调用时一行行打印。
2. 在@2处,因为yield return存在,是取到一条数据,打印一下。比如先取到hello(可能是下载网页),打印。然后取yzk(可能下载网页),打印yzk...。这样数据操作流水化了,取一点执行一点。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 在C# 8.0之前,async方法中不能用yield。从C# 8.0开始,把返回值声明为IAsyncEnumerable(不要带Task),然后遍历的时候用await foreach()即可。
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Demo02
{
class Program
{
static async Task Main(string[] args)
{
await foreach (var s in Test()) // foreach前要加await
{
Console.WriteLine(s);
}
}
static async IAsyncEnumerable<string> Test() // 注意,这里IAsyncEnumerable前面没有Task。
{
yield return "hello";
yield return "yzk";
yield return "youzack";
}
}
}
控制台输出:
hello
yzk
youzack
注意:static async Task<Enumerable<string>> Test() { yield ...},编译器会直接报错。
补充:这种写法用的较少,几乎不用。
2.3 不要同步异步混用
- ASP.NET Core和控制台项目中,没有SynchronizationContext,因此不用管ConfigureAwait(false)等,在WinForm项目中有。
- 不要同步、异步混用;尽量的有异步,用异步方法。
结尾
书籍:ASP.NET Core技术内幕与项目实战
视频:https://www.bilibili.com/video/BV1pK41137He
著:杨中科
ISBN:978-7-115-58657-5
版次:第1版
发行:人民邮电出版社
※敬请购买正版书籍,侵删请联系85863947@qq.com※
※本文章为看书或查阅资料而总结的笔记,仅供参考,如有错误请留言指正,谢谢!※