C#多线程(13):任务基础①

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

C#多线程(13):任务基础①

痴者工良   2020-04-28 我要评论
[TOC] ## 多线程编程 ### 多线程编程模式 .NET 中,有三种异步编程模式,分别是基于任务的异步模式(TAP)、基于事件的异步模式(EAP)、异步编程模式(APM)。 - **基于任务的异步模式 (TAP)** :.NET 推荐使用的异步编程方法,该模式使用单一方法表示异步操作的开始和完成。包括我们常用的 async 、await 关键字,属于该模式的支持。 - 基于事件的异步模式 (EAP) :是提供异步行为的基于事件的旧模型。[《C#多线程(12):线程池》](https://www.cnblogs.com/whuanle/p/12787505.html#不支持的线程池异步委托)中提到过此模式,.NET Core 已经不支持。 - 异步编程模型 (APM) 模式:也称为 [IAsyncResult](https:/https://img.qb5200.com/download-x/docs.microsoft.com/zh-cnhttps://img.qb5200.com/download-x/dotnet/api/system.iasyncresult) 模式,,这是使用 IAsyncResult 接口提供异步行为的旧模型。.NET Core 也不支持,请参考 [《C#多线程(12):线程池》](https://www.cnblogs.com/whuanle/p/12787505.html#不支持的线程池异步委托)。 前面,我们学习了三部分的内容: - 线程基础:如果创建线程、获取线程信息以及等待线程完成任务; - 线程同步:探究各种方式实现进程和线程同步,以及线程等待; - 线程池:线程池的优点和使用方法,基于任务的操作; 这篇开始探究任务和异步,而任务和异步是十分复杂的,内容错综复杂,笔者可能讲不好。。。 ### 探究优点 在前面中,学习多线程(线程基础和线程同步),一共写了 10 篇,写了这么多代码,我们现在来探究一下多线程编程的复杂性。 1. 传递数据和返回结果 传递数据倒是没啥问题,只是难以获取到线程的返回值,处理线程的异常也需要技巧。 2. 监控线程的状态 新建新的线程后,如果需要确定新线程在何时完成,需要自旋或阻塞等方式等待。 3. 线程安全 设计时要考虑如果避免死锁、合理使用各种同步锁,要考虑原子操作,同步信号的处理需要技巧。 4. 性能 玩多线程,最大需求就是提升性能,但是多线程中有很多坑,使用不当反而影响性能。
[以上总结可参考《C# 7.0本质论》19.3节,《C# 7.0核心技术指南》14.3 节]
我们通过使用线程池,可以解决上面的部分问题,但是还有更加好的选择,就是 Task(任务)。另外 Task 也是异步编程的基础类型,后面很多内容要围绕 Task 展开。 原理的东西,还是多参考微软官方文档和书籍,笔者讲得不一定准确,而且不会深入说明这些。 ## 任务操作 任务(Task)实在太多 API 了,也有各种骚操作,要讲清楚实在不容易,我们要慢慢来,一点点进步,一点点深入,多写代码测试。 下面与笔者一起,一步步熟悉、摸索 Task 的 API。 ### 两者创建任务的方式 通过其构造函数创建一个任务,其构造函数定义为: ```csharp public Task (Action action); ``` 其示例如下: ```csharp class Program { static void Main() { // 定义两个任务 Task task1 = new Task(()=> { Console.WriteLine("① 开始执行"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("① 执行中"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("① 执行即将结束"); }); Task task2 = new Task(MyTask); // 开始任务 task1.Start(); task2.Start(); Console.ReadKey(); } private static void MyTask() { Console.WriteLine("② 开始执行"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("② 执行中"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("② 执行即将结束"); } } ``` `.Start()` 方法用于启动一个任务。微软文档解释:启动 Task,并将它安排到当前的 TaskScheduler 中执行。 TaskScheduler 这个东西,我们后面讲,别急。 另一种方式则使用 `Task.Factory`,此属性用于创建和配置 `Task` 和 `Task` 实例的工厂方法。 使用https:/https://img.qb5200.com/download-x/docs.microsoft.com/zh-cnhttps://img.qb5200.com/download-x/dotnet/api/system.threading.tasks.taskfactory.startnew?view=netcore-3.1#--可以添加任务。

当需要对长时间运行、计算限制的任务(计算密集型)进行精细控制时才使用 StartNew() 方法;
官方推荐使用 Task.Run 方法启动计算限制任务。
Task.Factory.StartNew() 可以实现比 Task.Run() 更细粒度的控制。

`Task.Factory.StartNew()` 的重载方法是真的多,你可以参考: [https:/https://img.qb5200.com/download-x/docs.microsoft.com/zh-cnhttps://img.qb5200.com/download-x/dotnet/api/system.threading.tasks.taskfactory.startnew?view=netcore-3.1#--](https:/https://img.qb5200.com/download-x/docs.microsoft.com/zh-cnhttps://img.qb5200.com/download-x/dotnet/api/system.threading.tasks.taskfactory.startnew?view=netcore-3.1#--) 这里我们使用两个重载方法编写示例: ```csharp public Task StartNew(Action action); ``` ```csharp public Task StartNew(Action action, TaskCreationOptions creationOptions); ``` 代码示例如下: ```csharp class Program { static void Main() { // 重载方法 1 Task.Factory.StartNew(() => { Console.WriteLine("① 开始执行"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("① 执行中"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("① 执行即将结束"); }); // 重载方法 1 Task.Factory.StartNew(MyTask); // 重载方法 2 Task.Factory.StartNew(() => { Console.WriteLine("① 开始执行"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("① 执行中"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("① 执行即将结束"); },TaskCreationOptions.LongRunning); Console.ReadKey(); } // public delegate void TimerCallback(object? state); private static void MyTask() { Console.WriteLine("② 开始执行"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("② 执行中"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("② 执行即将结束"); } } ``` 通过 `Task.Factory.StartNew()` 方法添加的任务,会进入线程池任务队列然后自动执行,不需要手动启动。 `TaskCreationOptions.LongRunning` 是控制任务创建特性的枚举,后面讲。 ### Task.Run() 创建任务 `Task.Run()` 创建任务,跟 `Task.Factory.StartNew()` 差不多,当然 `Task.Run()` 还有很多重载方法和骚操作,我们后面再来学。 `Task.Run()` 创建任务示例代码如下: ```csharp static void Main() { Task.Run(() => { Console.WriteLine("① 开始执行"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("① 执行中"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("① 执行即将结束"); }); Console.ReadKey(); } ``` ### 取消任务和控制任务的创建 取消任务,[《C#多线程(12):线程池》](https://www.cnblogs.com/whuanle/p/12787505.html#任务取消功能) 中说过一次,不过控制太自由,全靠任务本身自觉判断是否取消。 这里我们通过 Task 来实现任务的取消,其取消是实时的、自动的,并且不需要手工控制。 其构造函数如下: ```csharp public Task StartNew(Action action, CancellationToken cancellationToken); ``` 代码示例如下: 按下回车键的时候记得切换字母模式。 ```csharp class Program { static void Main() { Console.WriteLine("任务开始启动,按下任意键,取消执行任务"); CancellationTokenSource cts = new CancellationTokenSource(); Task.Factory.StartNew(MyTask, cts.Token); Console.ReadKey(); cts.Cancel(); // 取消任务 Console.ReadKey(); } // public delegate void TimerCallback(object? state); private static void MyTask() { Console.WriteLine(" 开始执行"); int i = 0; while (true) { Console.WriteLine($" 第{i}次任务"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine(" 执行中"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine(" 执行结束"); i++; } } } ``` 前面创建任务的时候,我们碰到了 `TaskCreationOptions.LongRunning` 这个枚举类型,这个枚举用于控制任务的创建以及设定任务的行为。 其枚举如下: | 枚举 | 值 | 说明 | | ------------------------------ | ---- | ------------------------------------------------------------ | | AttachedToParent | 4 | 指定将任务附加到任务层次结构中的某个父级。 | | DenyChildAttach | 8 | 指定任何尝试作为附加的子任务执行的子任务都无法附加到父任务,会改成作为分离的子任务执行。 | | HideScheduler | 16 | 防止环境计划程序被视为已创建任务的当前计划程序。 | | LongRunning | 2 | 指定任务将是长时间运行的、粗粒度的操作,涉及比细化的系统更少、更大的组件。 | | None | 0 | 指定应使用默认行为。 | | PreferFairness | 1 | 提示 TaskScheduler 以一种尽可能公平的方式安排任务。 | | RunContinuationsAsynchronously | 64 | 强制异步执行添加到当前任务的延续任务。 | 这个枚举在 `TaskFactory` 和 `TaskFactory` 、`Task` 和 `Task` 、 `StartNew()`、`FromAsync()` 、`TaskCompletionSource` 等地方可以使用到。 这里来探究 `TaskCreationOptions.AttachedToParent`的使用。代码示例如下: ```csharp static void Main() { //两个任务没有从属关系,是独立的 Task task = new Task(() => { // 非子任务 Task task1 = new Task(() => { Thread.Sleep(TimeSpan.FromSeconds(1)); for (int i = 0; i < 5; i++) { Console.WriteLine("内层任务1"); Thread.Sleep(TimeSpan.FromSeconds(0.5)); } }); task1.Start(); for (int i = 0; i < 5; i++) { Console.WriteLine("外层任务"); Thread.Sleep(TimeSpan.FromSeconds(0.5)); } }); task.Start(); task.Wait(); Console.WriteLine("\n-------------------\n"); // 父子任务 task = new Task(() => { // TaskCreationOptions.AttachedToParent // 将此任务附加到父任务中 // 父任务需要等待所有子任务完成后,才能继续往下执行 Task task1 = new Task(() => { Thread.Sleep(TimeSpan.FromSeconds(1)); for (int i = 0; i < 5; i++) { Console.WriteLine(" 内层任务1"); Thread.Sleep(TimeSpan.FromSeconds(0.5)); } }, TaskCreationOptions.AttachedToParent); task1.Start(); Console.WriteLine("最外层任务"); Thread.Sleep(TimeSpan.FromSeconds(1)); }); task.Start(); task.Wait(); Console.ReadKey(); } ``` 而 `TaskCreationOptions.DenyChildAttach` 则不允许其它任务附加到外层任务中。 ```csharp static void Main() { // 不允许出现父子任务 Task task = new Task(() => { // TaskCreationOptions.AttachedToParent // 将此任务附加到父任务中 // 父任务需要等待所有子任务完成后,才能继续往下执行 Task task1 = new Task(() => { Thread.Sleep(TimeSpan.FromSeconds(1)); for (int i = 0; i < 5; i++) { Console.WriteLine(" 内层任务1"); Thread.Sleep(TimeSpan.FromSeconds(0.5)); } }, TaskCreationOptions.AttachedToParent); task1.Start(); Console.WriteLine("最外层任务"); Thread.Sleep(TimeSpan.FromSeconds(1)); }, TaskCreationOptions.AttachedToParent); // 不收儿子 task.Start(); task.Wait(); Console.ReadKey(); } ``` 然后,这里也学习了一个新的 Task 方法:`Wait()` 等待 Task 完成执行过程。`Wait()` 也可以设置超时时间。 TaskCreationOptions 枚举适合用来组合任务。 ### 任务返回结果以及异步获取返回结果 要获取任务返回结果,要使用泛型类或方法创建任务,例如 `Task`、`Task.Factory.StartNew()`、`Task.Run`。 通过 其泛型的 的 `Result` 属性,可以获得返回结果。 异步获取任务执行结果: ```csharp class Program { static void Main() { // ******************************* Task task = new Task(() => { return 666; }); // 执行 task.Start(); // 获取结果,属于异步 int number = task.Result; // ******************************* task = Task.Factory.StartNew(() => { return 666; }); // 也可以异步获取结果 number = task.Result; // ******************************* task = Task.Run(() => { return 666; }); // 也可以异步获取结果 number = task.Result; Console.ReadKey(); } } ``` 如果要同步的话,可以改成: ```csharp int number = Task.Factory.StartNew(() => { return 666; }).Result; ``` ### 捕获任务异常 进行中的任务发生了异常,不会直接抛出来阻止主线程执行,当获取任务处理结果或者等待任务完成时,异常会重新抛出。 示例如下: ```csharp static void Main() { // ******************************* Task task = new Task(() => { throw new Exception("反正就想弹出一个异常"); }); // 执行 task.Start(); Console.WriteLine("任务中的异常不会直接传播到主线程"); Thread.Sleep(TimeSpan.FromSeconds(1)); // 当任务发生异常,获取结果时会弹出 int number = task.Result; // task.Wait(); 等待任务时,如果发生异常,也会弹出 Console.ReadKey(); } ``` 乱抛出异常不是很好的行为噢~可以改成如下: ```csharp static void Main() { Task task = new Task(() => { try { throw new Exception("反正就想弹出一个异常"); return new Program(); } catch { return null; } }); task.Start(); var result = task.Result; if (result is null) Console.WriteLine("任务执行失败"); else Console.WriteLine("任务执行成功"); Console.ReadKey(); } ``` ### 全局捕获任务异常 `TaskScheduler.UnobservedTaskException` 是一个事件,其委托定义如下: ```csharp public delegate void EventHandler(object? sender, TEventArgs e); ``` 下面是一个示例: 请发布程序后,打开目录执行程序。 ```csharp class Program { static void Main() { TaskScheduler.UnobservedTaskException += MyTaskException; Task.Factory.StartNew(() => { throw new ArgumentNullException(); }); Thread.Sleep(100); GC.Collect(); GC.WaitForPendingFinalizers(); Console.WriteLine("Done"); Console.ReadKey(); } public static void MyTaskException(object sender, UnobservedTaskExceptionEventArgs eventArgs) { // eventArgs.SetObserved(); ((AggregateException)eventArgs.Exception).Handle(ex => { Console.WriteLine("Exception type: {0}", ex.GetType()); return true; }); } } ``` TaskScheduler.UnobservedTaskException 到底怎么用,笔者不太清楚。而且效果难以观察。 请参考: [https://stackoverflow.com/search?q=TaskScheduler.UnobservedTaskException](https://stackoverflow.com/search?q=TaskScheduler.UnobservedTaskException)

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们