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※

※本文章为看书或查阅资料而总结的笔记,仅供参考,如有错误请留言指正,谢谢!※

posted @ 2025-08-26 19:59  qinway  阅读(6)  评论(0)    收藏  举报