取消令牌(CancellationToken) 是 .Net Core 中的一项重要功能,正确并合理的使用 CancellationToken 可让业务达到简化代码、提高服务性能的效果;当在业务开发中,须要对一些特定的应用场景进行深度干预的时候,CancellationToken 将发挥很是重要的做用。html
在一个很常见的业务场景中,好比当请求一个文章详细信息的时候,须要同时加载部分点赞用户和评论内容,这里一共有 3 个任务,若是按照常规的先请求文章信息,而后再执行请求点赞和评论,那么咱们须要逐一的按顺序去数据库中执行 3 次查询;可是利用 CancellationToken ,咱们能够对这 3 个请求同时执行,而后在全部数据源都请求完成的时候,将这些数据进行合并,而后输出到客户端git
public static void Test() { Random rand = new Random(); CancellationTokenSource cts = new CancellationTokenSource(); List<Task<Article>> tasks = new List<Task<Article>>(); TaskFactory factory = new TaskFactory(cts.Token); foreach (var t in new string[] { "Article", "Post", "Love" }) { Console.WriteLine("开始请求"); tasks.Add(factory.StartNew(() => { var article = new Article { Type = t }; if (t == "Article") { article.Data.Add("文章已加载"); } else { for (int i = 1; i < 5; i++) { Thread.Sleep(rand.Next(1000, 2000)); Console.WriteLine("load:{0}", t); article.Data.Add($"{t}_{i}"); } } return article; }, cts.Token)); } Console.WriteLine("开始合并结果"); foreach (var task in tasks) { Console.WriteLine(); var result = task.Result; foreach (var d in result.Data) { Console.WriteLine("{0}:{1}", result.Type, d); } task.Dispose(); } cts.Cancel(); cts.Dispose(); Console.WriteLine("\nIsCancellationRequested:{0}", cts.IsCancellationRequested); }
上面的代码定义了一个 Test() 方法,在方法内部,首先定义了一个 CancellationTokenSource 对象,该退出令牌源内部建立了一个取消令牌属性 Token ;接下来,使用 TaskFacory 任务工厂建立了 3 个并行任务,并把这个任务存入 List<Task
> 列表对象中,在任务开始后,立刻迭代 tasks 列表,经过同步获取每一个任务的执行 Result 结果,在取消令牌没有收到取消通知的时候,任务将正常的执行下去,在全部任务都执行完成后,将 3 个请求结果输出到控制台中,同时销毁任务释放线程资源;最后,执行 cts.Cancel()取消令牌并释放资源,最后一句代码将输出令牌的状态。 github
经过上面的输出接口,能够看出,红色部分是模拟请求,这个请求时多线程进行的,Post 和 Love 交替出现,是由于在程序中经过线程休眠的方式模拟网络阻塞过程,蓝色为合并结果部分,能够看到,虽然“文章信息”已经加载完成,可是由于 Post 和 Love 还在请求中,因为取消令牌未收到退出通知,因此合并结果会等待信号,在全部线程都执行完成后,经过 cts.Cancel() 通知令牌取消,全部事件执行完成,控制台打印结果黄色部分为令牌状态,显示为 True ,令牌已取消。数据库
在某些场景中,咱们须要请求外部的第三方资源,好比请求天气预报信息;可是,因为网络等缘由,可能会形成长时间的等待以至业务超时退出,这种状况可使用 CancellationToken 来进行优化,但请求超过指定时长后退出,而没必要针对每一个 HttpClient 进行单独的超时设置网络
public async static Task GetToday() { CancellationTokenSource cts = new CancellationTokenSource(); cts.CancelAfter(3000); HttpClient client = new HttpClient(); var res = await client.GetAsync("http://www.weather.com.cn/data/sk/101110101.html", cts.Token); var result = await res.Content.ReadAsStringAsync(); Console.WriteLine(result); cts.Dispose(); client.Dispose(); }
在上面的代码中,首先定义了一个 CancellationTokenSource 对象,而后立刻发起了一个 HttpClient 的 GetAsync 请求(注意,这种使用 HttpClient 的方式是不正确的,详见个人博客 HttpClient的演进和避坑 ;在 GetAsync 请求中传入了一个取消令牌,而后当即发起了退出请求 Console.WriteLine(result); 无论 3 秒后请求是否返回,都将取消令牌等待信号,最后输出结果释放资源多线程
- 注意:若是是由于取消令牌退出引发请求中断,将会抛出任务取消的异常 TaskCanceledException
可使用建立一组令牌,经过连接各个令牌,使其创建通知关联,当 CancellationToken 链中的某个令牌收到取消通知的时候,由链式中建立出来的 CancellationToken 令牌也将同时取消dom
public async static Task Test() { CancellationTokenSource cts1 = new CancellationTokenSource(); CancellationTokenSource cts2 = new CancellationTokenSource(); var cts3 = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token); cts1.Token.Register(() => { Console.WriteLine("cts1 Canceling"); }); cts2.Token.Register(() => { Console.WriteLine("cts2 Canceling"); }); cts2.CancelAfter(1000); cts3.Token.Register(() => { Console.WriteLine("root Canceling"); }); var res = await new HttpClient().GetAsync("http://www.weather.com.cn/data/sk/101110101.html", cts1.Token); var result = await res.Content.ReadAsStringAsync(); Console.WriteLine("cts1:{0}", result); var res2 = await new HttpClient().GetAsync("http://www.weather.com.cn/data/sk/101110101.html", cts2.Token); var result2 = await res2.Content.ReadAsStringAsync(); Console.WriteLine("cts2:{0}", result2); var res3 = await new HttpClient().GetAsync("http://www.weather.com.cn/data/sk/101110101.html", cts3.Token); var result3 = await res2.Content.ReadAsStringAsync(); Console.WriteLine("cts3:{0}", result3); }
上面的代码定义了 3 个 CancellationTokenSource ,分别是 cts1,cts2,cts3,每一个 CancellationTokenSource 分别注册了 Register 取消回调委托,而后,使用 HttpClient 发起 3 组网络请求;其中,设置 cts2 在请求开始 1秒 后退出,预期结果为:当 cts2 退出后,因为 cts3 是使用 CreateLinkedTokenSource(cts1.Token, cts2.Token) 建立出来的,因此 cts3 应该也会被取消,实际上,不管 cts1/cts2 哪一个令牌取消,cts3 都会被取消异步
从上图能够看到,红色部分输出结果是:首先 cts2 取消,接着产生了链式反应致使 cts3 也跟着取消,蓝色部分为 cts1 的正常请求结果,最后输出了任务退出的异常信息async
CancellationToken 定义了三种不一样的取消方法,分别是 Cancel(),CancelAfter(),Dispose();这三种方式都表明了不一样的行为方式性能
public static void Test() { CancellationTokenSource cts1 = new CancellationTokenSource(); cts1.Token.Register(() => { Console.WriteLine("\ncts1 ThreadId: {0}", System.Threading.Thread.CurrentThread.ManagedThreadId); }); cts1.Cancel(); Console.WriteLine("cts1 State:{0}", cts1.IsCancellationRequested); CancellationTokenSource cts2 = new CancellationTokenSource(); cts2.Token.Register(() => { Console.WriteLine("\ncts2 ThreadId: {0}", System.Threading.Thread.CurrentThread.ManagedThreadId); }); cts2.CancelAfter(500); System.Threading.Thread.Sleep(1000); Console.WriteLine("cts2 State:{0}", cts2.IsCancellationRequested); CancellationTokenSource cts3 = new CancellationTokenSource(); cts3.Token.Register(() => { Console.WriteLine("\ncts3 ThreadId: {0}", System.Threading.Thread.CurrentThread.ManagedThreadId); }); cts3.Dispose(); Console.WriteLine("\ncts3 State:{0}", cts3.IsCancellationRequested); }
上面的代码定义了 3 个 CancellationTokenSource,分别是 cts1/cts2/cts3;分别执行了 3 中不一样的取消令牌的方式,并在取消回调委托中输出线程ID,从输出接口中看出,当程序执行 cts1.Cancel() 方法后,取消令牌当即执行了回调委托,并输出线程ID为:1;cts2.CancelAfter(500) 表示 500ms 后取消,为了得到令牌状态,这里使线程休眠了 1000ms,而 cts3 则直接调用了 Dispose() 方法,从输出结果看出,cts1 运行在和 Main 方法在同一个线程上,线程 ID 都为 1,而 cts2 因为使用了延迟取消,致使其在内部新建立了一个线程,其线程 ID 为 4;最后,cts3因为直接调用了 Dispose() 方法,可是其 IsCancellationRequested 的值为 False,表示未取消,而输出结果也代表,没有执行回调委托
https://github.com/lianggx/EasyAspNetCoreDemo/tree/master/Ron.ThreadingDemo