〇、前言

Task 是微软在 .Net 4.0 时代推出来的,也是微软极力推荐的一种多线程的处理方式。

在 Task 之前有一个高效多线程操作累 ThreadPool,虽然线程池相对于 Thread,具有很多优势避免频繁创建和销毁线程等,但是线程池也有一些使用上的不便,比如不支持取消、完成、失败通知等,也不支持线程执行的先后顺序配置。

为了解决上述痛点,Task 诞生了。Task 就是站在巨人的肩膀上而生,它是基于 ThreadPool 封装。Task 的控制和扩展性很强,在线程的延续、阻塞、取消、超时等方面远胜于 ThreadPool。

本文将对 Task 进行一个详细的介绍。

一、任务如何创建和启动?

创建任务执行任务是可以分离的,也可以同时进行。如下代码有四种开启任务的方式:

  • 第一种:任务 t1 通过调用 Task 类构造函数进行实例化,但仅在任务 t2 启动后调用其 Start() 方法启动。【创建+未启动】
  • 第二种:任务 t2 通过调用 TaskFactory.StartNew(Action<Object>, Object) 方法在单个方法调用中实例化和启动。【创建+启动】
  • 第三种:任务 t3 通过调用 Run(Action) 方法在单个方法调用中实例化和启动。【创建+启动】
  • 第四种:任务 t4 通过调用 RunSynchronously() 方法在主线程上同步执行。【创建+未启动】
  1. static void Main(string[] args)
  2. {
  3. // 用于异步调用的委托函数,接受类型为 Object 的参数
  4. Action<object> action = (object obj) =>
  5. {
  6. // Task.CurrentId :任务 ID
  7. // Thread.CurrentThread.ManagedThreadId :线程 ID
  8. Console.WriteLine($"Task={Task.CurrentId}, obj={obj}, Thread={Thread.CurrentThread.ManagedThreadId}");
  9. // throw new Exception();
  10. };
  11. // 【第一种】创建一个就绪,但【未启动】的任务,需要在后文通过 t1.Start() 启动
  12. Task t1 = new Task(action, "甲"); // alpha:初始
  13. // 【第二种】【创建并启动】一个任务
  14. Task t2 = Task.Factory.StartNew(action, "乙");
  15. // 占用主线程,等待任务 t2 完成
  16. t2.Wait();
  17. // 启动第一个任务 t1
  18. t1.Start();
  19. Console.WriteLine($"t1 已启动 (主线程 = {Thread.CurrentThread.ManagedThreadId})");
  20. // 通过 Wait() 占用主线程,等待 t1 执行完毕
  21. t1.Wait();
  22. // 【第三种】通过 Task.Run() 【创建并启动】一个任务
  23. string taskData = "丙";
  24. Task t3 = Task.Run(() =>
  25. {
  26. Console.WriteLine($"Task={Task.CurrentId}, obj={taskData}, Thread={Thread.CurrentThread.ManagedThreadId}");
  27. });
  28. // 通过 Wait() 占用主线程,等待 t3 执行完毕
  29. t3.Wait();
  30. // 【第四种】创建一个就绪,但【未启动】的任务 t4
  31. Task t4 = new Task(action, "丁");
  32. // Synchronously:同步的
  33. // 开启同步任务 t4,在主线程上运行
  34. t4.RunSynchronously();
  35. // t4 是以同步的方式运行的,此时的 Wait() 可以捕捉到异常
  36. t4.Wait();
  37. Console.ReadLine();
  38. }

如下图输出结果,最先开启的 t2,由于是工厂中启动的,所以不占用主线程运行。Task.Run() 同样是非主线程运行,但它并未新开线程,而是直接用了 t2 执行的线程。

线程编号为 1 的是主线程,t1 是主线程最先创建的,所以直接由主线程运行。t4 是在同步执行的任务,因此也是主线程来执行。

  

二、等待一个或多个任务

用于等待任务的方法有很多个,如下:

Wait() task1.Wait() 单线程等待
WaitAll() Task.WaitAll(tasks) 等待任务集合 tasks 中的全部任务完成
WaitAny()  int index = Task.WaitAny(tasks) 等待任一任务完成,并返回这一任务的编号
WhenAll() Task t = Task.WhenAll(tasks) 返回一个新的任务,这个任务的完成状态在【tasks 集合中全部任务都完成时】完成
WhenAny() Task t = Task.WhenAny(tasks) 返回在任务集合 tasks 中第一个执行完成的任务对象

下面几个示例来实操下。

2.1 Wait()

对于 Wait() 单线程等待,没啥好说的,看代码:

  1. static void Main(string[] args)
  2. {
  3. // 创建并执行一个任务执行匿名函数
  4. Task taskA = Task.Run(() => Thread.Sleep(2000));
  5. Console.WriteLine($"taskA Status: {taskA.Status}"); // taskA Status: WaitingToRun
  6. try
  7. {
  8. taskA.Wait(1000); // 主线程等待任务 1s 此时任务尚未完成
  9. Console.WriteLine($"taskA Status: {taskA.Status}"); // taskA Status: Running
  10. taskA.Wait(); // 线程等待任务 taskA 完成
  11. Console.WriteLine($"taskA Status: {taskA.Status}"); // taskA Status: RanToCompletion
  12. }
  13. catch (AggregateException)
  14. {
  15. Console.WriteLine("Exception in taskA.");
  16. }
  17. }

2.2 Wait(Int32, CancellationToken)  支持手动取消

关于 Wait(Int32, CancellationToken) 任务可手动取消的重载。在任务完成之前,超时或调用了 Cancel() 方法,等待终止。

如下示例,一个线程一个任务,线程中将 CabcellationTokenSource 的实例 cts 取消掉,导致后续任务等待时调用 cts.Token 导致异常 OperationCanceledException 的发生。

  1. static void Main(string[] args)
  2. {
  3. CancellationTokenSource cts = new CancellationTokenSource();
  4. Thread thread = new Thread(CancelToken); // 新开一个线程执行方法:CancelToken()
  5. thread.Start(cts);
  6. Task t = Task.Run(() => // 新增一个任务执行匿名函数
  7. {
  8. Task.Delay(5000).Wait(); // 延迟等待 5s
  9. Console.WriteLine("Task ended delay...");
  10. });
  11. try
  12. {
  13. Console.WriteLine($"About to wait completion of task {t.Id}"); // 以上两个操作都有延迟,所以此处消息先打印
  14. // 等待任务 t 1.51s,保证线程已执行完成,就是保证 CancellationTokenSource 已执行过取消操作
  15. // 由于 cts 已经取消,因此次数就抛异常:OperationCanceledException
  16. bool result = t.Wait(1510, cts.Token); // 后边代码就不再执行,直接跳到 catch
  17. Console.WriteLine($"Wait completed normally: {result}");
  18. Console.WriteLine($"The task status: {t.Status}");
  19. }
  20. catch (OperationCanceledException e)
  21. {
  22. Console.WriteLine($"{e.GetType().Name}: The wait has been canceled.");
  23. Console.WriteLine($"Task status:{t.Status}"); // 此时程序运行 1.5s 多,任务 t 还在等待,因此状态是 Running
  24. Thread.Sleep(4000); // 4s + 1.5s > 5s 此时任务 t 已经执行完成,状态为 RanToCompletion
  25. Console.WriteLine("After sleeping, the task status: {t.Status}");
  26. cts.Dispose();
  27. }
  28. Console.ReadLine();
  29. }
  30. private static void CancelToken(Object obj)
  31. {
  32. Thread.Sleep(1500); // 延迟 1.5s
  33. Console.WriteLine($"Canceling the cancellation token from thread {Thread.CurrentThread.ManagedThreadId}...");
  34. CancellationTokenSource source = obj as CancellationTokenSource;
  35. if (source != null)
  36. source.Cancel(); // 将 CancellationTokenSource 的实例执行取消
  37. }

  

2.3 WaitAll()

等待一组任务全部完成,无论是否抛异常。AggregateException 将会收集全部异常信息,可以通过遍历获取每一个异常详情。

如下代码,新建是个任务组成任务组 tasks,其中 2~5 线程手动抛异常,最后通过遍历 AggregateException aex 记录全部异常。

  1. static void Main(string[] args)
  2. {
  3. var tasks = new List<Task<int>>();
  4. // 创建一个委托,用于任务执行,并记录每个任务信息
  5. Func<object, int> action = (object obj) =>
  6. {
  7. int i = (int)obj;
  8. // 让每次的 TickCount 不同(系统开始运行的毫秒数)
  9. Thread.Sleep(i * 1000);
  10. if (2 <= i && i <= 5) // 从第 2 到 5 个任务都抛异常
  11. {
  12. throw new InvalidOperationException("SIMULATED EXCEPTION");
  13. }
  14. int tickCount = Environment.TickCount; // 获取系统开始运行的毫秒数
  15. Console.WriteLine($"Task={Task.CurrentId}, i={i}, TickCount={tickCount}, Thread={Thread.CurrentThread.ManagedThreadId}");
  16. return tickCount;
  17. };
  18. // 连续创建 10 个任务
  19. for (int i = 0; i < 10; i++)
  20. {
  21. int index = i;
  22. tasks.Add(Task<int>.Factory.StartNew(action, index)); // 后台线程
  23. }
  24. try
  25. {
  26. // WaitAll() 等待全部任务完成
  27. Task.WaitAll(tasks.ToArray());
  28. // 由于线程中手动抛出了异常,因此这个消息将无法打印在控制台
  29. Console.WriteLine("WaitAll() has not thrown exceptions. THIS WAS NOT EXPECTED.");
  30. }
  31. catch (AggregateException aex) // AggregateException 异常中包含 2~5 四个异常
  32. {
  33. Console.WriteLine("\nThe following exceptions have been thrown by WaitAll(): (THIS WAS EXPECTED)");
  34. Console.WriteLine($"\ne.InnerExceptions.Count:{aex.InnerExceptions.Count}");
  35. for (int j = 0; j < aex.InnerExceptions.Count; j++) // aex.InnerExceptions.Count == 4
  36. {
  37. Console.WriteLine("\n-------------------------------------------------\n{0}", aex.InnerExceptions[j].ToString());
  38. }
  39. }
  40. Console.ReadLine();
  41. }

  

2.4 WaitAny()

等待一组任务中的任一任务完成,然后返回第一个执行完成任务的序号,可通过tasks[index].Id取得任务 ID。

如下示例,每个任务都有延迟,当第一个任务完成时,遍历打印出其他全部任务的状态:

  1. static void Main(string[] args)
  2. {
  3. Task[] tasks = new Task[5];
  4. for (int ctr = 0; ctr <= 4; ctr++)
  5. {
  6. int factor = ctr; // 重新声明一个变量
  7. tasks[ctr] = Task.Run(() => Thread.Sleep(factor * 250 + 50));
  8. }
  9. int index = Task.WaitAny(tasks); // 等待任一任务结束
  10. Console.WriteLine($"任务 #{tasks[index].Id} 已完成。");
  11. Console.WriteLine("\n当前各个任务的状态:");
  12. foreach (var t in tasks)
  13. Console.WriteLine($" Task {t.Id}: {t.Status}");
  14. Console.ReadLine();
  15. }

  

参考:https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.task.wait?view=net-7.0

三、延续任务 Task.ContinueWith()

3.1 一个简单的示例

如下代码,首先创建一个耗时的任务 task 并启动,此时也不影响主线程的运行。然后通过task.ContinueWith()在第一个任务执行完成后,执行其中的匿名函数。

  1. static void Main(string[] args)
  2. {
  3. // 创建一个任务
  4. Task<int> task = new Task<int>(() =>
  5. {
  6. int sum = 0;
  7. Console.WriteLine($"使用 Task 执行异步操作,当前线程 {Thread.CurrentThread.ManagedThreadId}");
  8. Thread.Sleep(2000);
  9. for (int i = 0; i < 100; i++)
  10. {
  11. sum += i;
  12. }
  13. return sum;
  14. });
  15. // 启动任务
  16. task.Start();
  17. // 主线程在此处可以执行其他处理
  18. Console.WriteLine($"1 主线程 {Thread.CurrentThread.ManagedThreadId}");
  19. Thread.Sleep(1000);
  20. //任务完成时执行处理。
  21. Task cwt = task.ContinueWith(t =>
  22. {
  23. Console.WriteLine($"任务完成后的执行结果:{t.Result} 当前线程 {Thread.CurrentThread.ManagedThreadId}");
  24. });
  25. task.Wait();
  26. cwt.Wait();
  27. Console.WriteLine($"2 主线程 {Thread.CurrentThread.ManagedThreadId}");
  28. Console.ReadLine();
  29. }

  

详情可参考:https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.task.continuewith?view=net-7.0

3.2 任务的并行与串行

ContinueWith、WaitAll 当这两者结合起来,我们就可以处理复杂一点的东西。比如,现在有 7 个任务,其中 t1 需要串行,t2-t3 可以并行,t4 需要串行,t5-t6 并行,t7 串行。逻辑如下图:

  

  1. public static void Main(string[] args)
  2. {
  3. ConcurrentStack<int> stack = new ConcurrentStack<int>(); // ConcurrentStack:线程安全的后进先出(LIFO:LastIn-FirstOut)集合
  4. ConcurrentBag<int> bag = new ConcurrentBag<int>(); // ConcurrentBag:线程安全的无序集合
  5. // t1先串行
  6. var t1 = Task.Factory.StartNew(() =>
  7. {
  8. stack.Push(1);
  9. stack.Push(2);
  10. });
  11. // t1.ContinueWith() t1 之后,t2、t3并行执行
  12. var t2 = t1.ContinueWith(t =>
  13. {
  14. int result;
  15. stack.TryPop(out result);
  16. });
  17. // t2,t3并行执行
  18. var t3 = t1.ContinueWith(t =>
  19. {
  20. int result;
  21. stack.TryPop(out result);
  22. });
  23. // 等待 t2、t3 执行完
  24. Task.WaitAll(t2, t3);
  25. //t4串行执行
  26. var t4 = Task.Factory.StartNew(() =>
  27. {
  28. stack.Push(1);
  29. stack.Push(2);
  30. });
  31. // t5、t6 并行执行
  32. var t5 = t4.ContinueWith(t =>
  33. {
  34. int result;
  35. stack.TryPop(out result);
  36. });
  37. // t5、t6 并行执行
  38. var t6 = t4.ContinueWith(t =>
  39. {
  40. int result;
  41. // 只弹出,不移除
  42. stack.TryPeek(out result);
  43. });
  44. // 临界区:等待 t5、t6 执行完
  45. Task.WaitAll(t5, t6);
  46. // t7 串行执行
  47. var t7 = Task.Factory.StartNew(() =>
  48. {
  49. Console.WriteLine($"当前集合元素个数:{stack.Count}"); // 当前集合元素个数:1
  50. });
  51. Console.ReadLine();
  52. }

参考: https://www.cnblogs.com/huangxincheng/archive/2012/04/03/2430638.html

https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.task?view=net-7.0

https://www.cnblogs.com/zhaoshujie/p/11082753.html

关于 Task 简单梳理的更多相关文章

  1. 机器学习&数据挖掘笔记(常见面试之机器学习算法思想简单梳理)

    机器学习&数据挖掘笔记_16(常见面试之机器学习算法思想简单梳理) 作者:tornadomeet 出处:http://www.cnblogs.com/tornadomeet 前言: 找工作时( ...

  2. [转]机器学习&数据挖掘笔记_16(常见面试之机器学习算法思想简单梳理)

    机器学习&数据挖掘笔记_16(常见面试之机器学习算法思想简单梳理) 转自http://www.cnblogs.com/tornadomeet/p/3395593.html 前言: 找工作时(I ...

  3. C#正则表达式_简单梳理_Emoji表情字符处理

    A-最近一直有接触到正则表达式,现对其做简单梳理: private const RegexOptions OPTIONS = RegexOptions.IgnoreCase | RegexOption ...

  4. RocketMQ 简单梳理 及 集群部署笔记【转】

    一.RocketMQ 基础知识介绍Apache RocketMQ是阿里开源的一款高性能.高吞吐量.队列模型的消息中间件的分布式消息中间件. 上图是一个典型的消息中间件收发消息的模型,RocketMQ也 ...

  5. Memcached概念、作用、运行原理、特性、不足简单梳理(1)

    大家可能对memcached这种产品早有了解,或者已经应用在自己的网站中了,但是也有一些朋友从来都没有听说过或者使用过.这都没什么关系,本文旨在从各个角度综合的介绍这种产品,尽量深入浅出,如果能对您现 ...

  6. cinder创建volume的流程-简单梳理

    1. cinder-api接收到创建的请求,入口:cinder.api.v2.volumes.VolumeController#create,该方法主要负责一些参数的重新封装和校验,然后调用cinde ...

  7. Gradle task简单使用

    还望支持个人博客站:http://www.enjoytoday.cn task是什么 task是gradle构建脚本的最小运行单元,我们通过在gradle脚本中创建task任务,以期完成某个特定的功能 ...

  8. 简单梳理JavaScript垃圾回收机制

    JavaScript具有自动垃圾回收机制,即执行环境会负责管理代码执行过程中使用地内存. 这种垃圾回收机制的原理很简单:找出那些不再继续使用的变量,然后释放其占用的内存.为此,垃圾收集器会按照固定的时 ...

  9. 简单梳理下 Vue3 的新特性

    在 Vue3 测试版刚刚发布的时候,我就学习了下 Composition API,但没想到正式版时隔一年多才出来,看了一下发现还是增加了不少新特性的,在这里我就将它们一一梳理一遍. 本文章只详细阐述 ...

  10. 机器学习&数据挖掘笔记_16(常见面试之机器学习算法思想简单梳理)

    前言: 找工作时(IT行业),除了常见的软件开发以外,机器学习岗位也可以当作是一个选择,不少计算机方向的研究生都会接触这个,如果你的研究方向是机器学习/数据挖掘之类,且又对其非常感兴趣的话,可以考虑考 ...

随机推荐

  1. Docker Compose 部署 Jenkins

    Jenkins介绍 Jenkins是一个开源软件项目,是基于Java开发的一种持续集成工具 Jenkins应用广泛,大多数互联网公司都采用Jenkins配合GitLab.Docker.K8s作为实现D ...

  2. linux下live555编译和调试

    linux下live555编译和调试 live555 支持 h.264 初步告捷,可以播放,尽管不是很稳定,或者说暂时只能播放 1 帧(主要是我现在还不了解 帧的概念),同时还有 Mal SDP 的传 ...

  3. [Python图像处理] 一.图像处理基础知识及OpenCV入门函数

    该系列文章是讲解Python OpenCV图像处理知识,前期主要讲解图像入门.OpenCV基础用法,中期讲解图像处理的各种算法,包括图像锐化算子.图像增强技术.图像分割等,后期结合深度学习研究图像识别 ...

  4. CF1477F Nezzar and Chocolate Bars 题解

    题意: 有一根长为 \(1\) 的巧克力,已经被切了 \(m-1\) 刀被分成 \(m\) 分,接下来每次在整根长度为 \(1\) 的巧克力上均匀随机一个点切一刀,求每一小段巧克力长度均小于一个给定值 ...

  5. [UR #14]人类补完计划

    计数好题. 题意:给定简单无向图 \(G=(V,E),|V|=n,|E|=m\),有 \(n\leq 16,m\leq {n\choose 2}\),求所有为基环树的子图的权值之和.一个基环树的权值定 ...

  6. P1980 [NOIP2013 普及组] 计数问题

    题目链接:https://www.luogu.com.cn/problem/P1980 术语 以下的英文术语均可以翻译为数字. digit: 一个数字字符,十进制就是 0-9 之间的一个字符: num ...

  7. 从案例中详解go-errgroup-源码

    一.背景 某次会议上发表了error group包,一个g失败,其他的g会同时失败的错误言论(看了一下源码中的一句话The first call to return a non-nil error c ...

  8. Linux xfs文件系统stat命令Birth字段为空的原因探究

    在Linux平台找出某个目录下创建时间最早的文件,测试验证脚本结果是否准确的过程中发现一个很有意思的现象,stat命令在一些平台下Birth字段有值,而在一些平台则为空值,如下所示: RHEL 8.7 ...

  9. 【HDU】1559 最大子矩阵 (二维前缀和,动态规划)

    动态规划之二维前缀和 题目 给你一个m×n的整数矩阵,在上面找一个x×y的子矩阵,使子矩阵中所有元素的和最大. 输入 输入数据的第一行为一个正整数T,表示有T组测试数据.每一组测试数据的第一行为四个正 ...

  10. flutter填坑之旅(有状态组件StatefulWidget)

    今天我们来看看flutter的StatefulWidget(有状态组件),最常用就是app 主页的底部导航栏的应用 效果图 首页 关于 我的 statefull-widget-learn .dart ...