C#多线程系列之任务基础(一)
作者:痴者工良 发布时间:2022-12-09 04:48:22
多线程编程
多线程编程模式
.NET 中,有三种异步编程模式,分别是基于任务的异步模式(TAP)、基于事件的异步模式(EAP)、异步编程模式(APM)。
基于任务的异步模式 (TAP) :.NET 推荐使用的异步编程方法,该模式使用单一方法表示异步操作的开始和完成。包括我们常用的 async 、await 关键字,属于该模式的支持。
基于事件的异步模式 (EAP) :是提供异步行为的基于事件的旧模型。《C#多线程(12):线程池》中提到过此模式,.NET Core 已经不支持。
异步编程模型 (APM) 模式:也称为 IAsyncResult 模式,,这是使用 IAsyncResult 接口提供异步行为的旧模型。.NET Core 也不支持,请参考 《C#多线程(12):线程池》。
前面,我们学习了三部分的内容:
线程基础:如何创建线程、获取线程信息以及等待线程完成任务;
线程同步:探究各种方式实现进程和线程同步,以及线程等待;
线程池:线程池的优点和使用方法,基于任务的操作;
这篇开始探究任务和异步,而任务和异步是十分复杂的,内容错综复杂,笔者可能讲不好。。。
探究优点
我们现在来探究一下多线程编程的复杂性。
传递数据和返回结果
传递数据倒是没啥问题,只是难以获取到线程的返回值,处理线程的异常也需要技巧。
监控线程的状态
新建新的线程后,如果需要确定新线程在何时完成,需要自旋或阻塞等方式等待。
线程安全
设计时要考虑如果避免死锁、合理使用各种同步锁,要考虑原子操作,同步信号的处理需要技巧。
性能
玩多线程,最大需求就是提升性能,但是多线程中有很多坑,使用不当反而影响性能。
[以上总结可参考《C# 7.0本质论》19.3节,《C# 7.0核心技术指南》14.3 节]
我们通过使用线程池,可以解决上面的部分问题,但是还有更加好的选择,就是 Task(任务)。另外 Task 也是异步编程的基础类型,后面很多内容要围绕 Task 展开。
原理的东西,还是多参考微软官方文档和书籍,笔者讲得不一定准确,而且不会深入说明这些。
任务操作
任务(Task)实在太多 API 了,也有各种骚操作,要讲清楚实在不容易,我们要慢慢来,一点点进步,一点点深入,多写代码测试。
下面与笔者一起,一步步熟悉、摸索 Task 的 API。
两种创建任务的方式
通过其构造函数创建一个任务,其构造函数定义为:
public Task (Action action);
其示例如下:
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<TResult>
实例的工厂方法。
使用https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.taskfactory.startnew?view=netcore-3.1#--可以添加任务。
当需要对长时间运行、计算限制的任务(计算密集型)进行精细控制时才使用 StartNew() 方法;
官方推荐使用 Task.Run 方法启动计算限制任务。
Task.Factory.StartNew() 可以实现比 Task.Run() 更细粒度的控制。
Task.Factory.StartNew()
的重载方法是真的多,你可以参考: https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.taskfactory.startnew?view=netcore-3.1#--
这里我们使用两个重载方法编写示例:
public Task StartNew(Action action);
public Task StartNew(Action action, TaskCreationOptions creationOptions);
代码示例如下:
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()
创建任务示例代码如下:
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):线程池》 中说过一次,不过控制太自由,全靠任务本身自觉判断是否取消。
这里我们通过 Task 来实现任务的取消,其取消是实时的、自动的,并且不需要手工控制。
其构造函数如下:
public Task StartNew(Action action, CancellationToken cancellationToken);
代码示例如下:
按下回车键的时候记得切换字母模式。
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<TResult>
、Task
和 Task<TResult>
、
StartNew()
、FromAsync()
、TaskCompletionSource<TResult>
等地方可以使用到。
子任务使用了 TaskCreationOptions.AttachedToParent ,并不是指父任务要等待子任务完成后,父任务才能继续完往下执行;而是指父任务如果先执行完毕,那么必须等待子任务完成后,父任务才算完成。
这里来探究 TaskCreationOptions.AttachedToParent
的使用。代码示例如下:
// 父子任务
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));
});
task.Start();
task.Wait();
Console.ReadKey();
而 TaskCreationOptions.DenyChildAttach
则不允许其它任务附加到外层任务中。
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));
}
}, TaskCreationOptions.AttachedToParent);
task1.Start();
Console.WriteLine("最外层任务");
Thread.Sleep(TimeSpan.FromSeconds(1));
}, TaskCreationOptions.DenyChildAttach); // 不收儿子
task.Start();
task.Wait();
Console.ReadKey();
}
然后,这里也学习了一个新的 Task 方法:Wait()
等待 Task 完成执行过程。Wait()
也可以设置超时时间。
如果父任务是通过调用 Task.Run 方法而创建的,则可以隐式阻止子任务附加到其中。
关于附加的子任务,请参考:https://docs.microsoft.com/zh-cn/dotnet/standard/parallel-programming/attached-and-detached-child-tasks?view=netcore-3.1
任务返回结果以及异步获取返回结果
要获取任务返回结果,要使用泛型类或方法创建任务,例如 Task<Tresult>
、Task.Factory.StartNew<TResult>()
、Task.Run<TResult>
。
通过 其泛型的 的 Result
属性,可以获得返回结果。
异步获取任务执行结果:
class Program
{
static void Main()
{
// *******************************
Task<int> task = new Task<int>(() =>
{
return 666;
});
// 执行
task.Start();
// 获取结果,属于异步
int number = task.Result;
// *******************************
task = Task.Factory.StartNew<int>(() =>
{
return 666;
});
// 也可以异步获取结果
number = task.Result;
// *******************************
task = Task.Run<int>(() =>
{
return 666;
});
// 也可以异步获取结果
number = task.Result;
Console.ReadKey();
}
}
如果要同步的话,可以改成:
int number = Task.Factory.StartNew<int>(() =>
{
return 666;
}).Result;
捕获任务异常
进行中的任务发生了异常,不会直接抛出来阻止主线程执行,当获取任务处理结果或者等待任务完成时,异常会重新抛出。
示例如下:
static void Main()
{
// *******************************
Task<int> task = new Task<int>(() =>
{
throw new Exception("反正就想弹出一个异常");
});
// 执行
task.Start();
Console.WriteLine("任务中的异常不会直接传播到主线程");
Thread.Sleep(TimeSpan.FromSeconds(1));
// 当任务发生异常,获取结果时会弹出
int number = task.Result;
// task.Wait(); 等待任务时,如果发生异常,也会弹出
Console.ReadKey();
}
乱抛出异常不是很好的行为噢~可以改成如下:
static void Main()
{
Task<Program> task = new Task<Program>(() =>
{
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
是一个事件,其委托定义如下:
public delegate void EventHandler<TEventArgs>(object? sender, TEventArgs e);
下面是一个示例:
请发布程序后,打开目录执行程序。
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://www.cnblogs.com/whuanle/p/12792639.html


猜你喜欢
- 最近在使用后端云Bmob对数据进行存储,目的是在不搭建服务器的前提下,能对Android应用的数据进行操作处理,其实这篇是比较久之前写的了,
- 在Android中,Activity主要负责前台页面的展示,Service主要负责需要长期运行的任务,所以在我们实际开发中,就会常常遇到Ac
- 试题描述:一个球场C的球迷看台可容纳M*N个球迷。官方想统计一共有多少球迷群体,最大的球迷群体有多少人。球迷选座特性:同球迷群体会选择相邻座
- 本文实例讲述了C#中Winform窗体Form的关闭按钮变灰色的方法,对C#程序设计有一定的借鉴价值,分享给大家供大家参考之用。具体方法如下
- 查看公司项目代码时,存在这样一个问题:winform界面上有很多信息填写,提交
- 一、程序环境以下内容通过C#及VB.NET代介绍如何给Excel文档添加数字签名,以及删除Excel文档中已有的数字签名。工具使用最近发布的
- Spring boot默认使用的是SimpleCacheConfiguration,即使用ConcurrentMapCacheManager
- 作为java中的一个重要理念,说起面向对象也是老生常谈了。在找资料的时候多是很专业的术语,又或者很多框架的知识点合集,其实大部分人刚看资料的
- 1、概述之前写了一个Android * QQ5.0 侧滑菜单效果 自定义控件来袭 ,恰逢QQ5.2又加了一个右侧菜单,刚好看了下Drawe
- 前言在H5火热的时代,许多框架都出了底部弹窗的控件,在H5被称为弹出菜单ActionSheet,今天我们也来模仿一个ios的底部弹窗,取材于
- 一、前言知识补充:Arrays.copyOf函数:public static int[] copyOf(int[] original, in
- 普通 jar 包的导出1.点击 file 中的project.structor=>选择Artifacts=>+=>选择 j
- 最近看了一些淘宝购物车的demo,于是也写了一个。效果图如下:主要代码如下: actvity中的代码:public class Shoppi
- 本文汇总了android 8种对话框(Dialog)使用方法,分享给大家供大家参考,具体内容如下1.写在前面Android提供了丰富的Dia
- 一、背景1.1、前言当我们写好代码并测试功能符合要求时,有可能每天都要执行这个程序(比如我写了一个爬虫脚本,每天定时运行获取我想看的小说更新
- 首先,ListView不能直接用,要自定义一个,然后重写onMeasure()方法:@Override protected vo
- 本文实例讲述了java简单列出文件夹下所有文件的方法。分享给大家供大家参考,具体如下:import Java.io.*;public cla
- SharedPreferences在开发过程中,数据存取是较为频繁的,今天我们来了解下android几种常见的数据存取方式。在Android
- 1.kotlin的字符串操作和Java有些不同,有些新增。1)先看字符串比较java中==比较的是变量的引用是否指向同一个地址,Kotlin
- 什么是Stream流?Stream流是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。Stream的优点:声明性,可复合,可并行。