- 4.1 Task 基础概念
- 4.1.1 Task 内部机制
- 4.1.2 创建一个 Task
- 4.2 Task 基本操作
- 4.2.1 获得 Task 执行结果
- 4.2.2 取消任务
- 4.2.3 ContinueWith 与新任务
- 4.2.4 父、子任务
4.1 Task 基础概念
为什么需要 Task 在 CLR 当中,已经提供了线程池、QueueUserWorkItem 来发起异步的计算限制操作,为什么还需要 Task?
- 原因一:更高程度的抽象
Task
提供了更高级别的抽象,使并行编程更加易于理解和维护;- 它是对线程的抽象,允许以更声明性的方式描述并行操作。这使得编写并行代码更加清晰,减少了线程管理的复杂性;
- 原因二:可以进行任务组合、取消操作
Task
支持任务的组合,可以轻松地创建任务依赖关系,例如顺序执行、并行执行、等待多个任务完成等;Task
支持任务的取消操作。可以通过CancellationToken
取消任务,而线程池不提供这种内置的取消机制;
- 原因三:异常处理
Task
提供了异常处理的机制,允许捕获和处理任务中抛出的异常。这比在线程池中使用传统的try-catch
块更加灵活和强大;
- 原因四:天生支持的协程及异步编程
Task
是支持协程和异步编程的基础。它允许使用async/await
关键字来编写更具表现力和可读性的异步代码,使异步操作更容易管理;
4.1.1 Task 内部机制
Task 内部字段
- 每个 Task 对象都有一组字段,这些字段构成了任务的状态;
- 其中包括
-
- Int32 的 ID
-
- 代表 Task 执行状态的一个 Int32 :Status
-
- 对父任务的引用
-
- 对 Task 创建时指定的 TaskScheduler 的引用
-
- 对回调方法的引用
-
- 对要传给回调方法的对象的引用
-
- 对 ExecutionContext 的引用
-
- 对 ManualResetEventSlim 对象的引用
-
Task 的唯一 ID
- 每个 Task 对象都包含代表 Task 唯一 ID 的 Int 32 字段,创建 Task 对象时该字段初始化为零;
- 首次查询 Task 的只读 ID 属性时,属性将一个唯一的 Int 32 值分配给该字段,并返回该值;
- 任务 ID 从 1 开始,每分配一个 ID 都递增 1;
- 注:在 Microsoft Visual Studio 调试器中查看 Task 对象,会造成调试器显示 Task 的 ID,从而造成为 Task 分配 ID。
- 使用此 ID 的意义,在于可以对每个 Task 进行唯一值标识;
Task 的只读 Status 属性
- 作用
- 可以通过查询 Task 的 Status 属性来了解当前 Task 处理生命周期的什么位置;
- TaskStatus 切换流程
- 状态 1:Created
- 首次构造 Task 后,将变为 Created 状态;
- 状态 2:WaitingToRun
- 任务启动后进入此状态
- 状态 3:Running
- 运行时的状态
- 状态 4:WaitingForChildrenToComplete
- 停止运行,并等待子任务
- 状态 5:RanToCompletion 运行完成 | Canceled 取消 | Faulted 出错
- 状态 1:Created
4.1.2 创建一个 Task
创建一个新的 Task
//写法1
Task t1 = new Task(ComputeBoundOp,5);
t1.Start();
//写法2
Task.Run(()=> ComputeBoundOp(5) );
Task 的构造函数
创建一个 Task 需要调用构造器,并传递一个 Action 或 Action<Object>
委托;
- 委托就是你想执行的操作;
- 如果传递的是 Object 的方法,还必须向 Task 的构造器传递最终要传给操作的实参;
- 调用 Run 时可以传递一个 Action 或
Func<TResult>
委托来指定想要执行的操作;
- 调用 Run 时可以传递一个 Action 或
- 无论调用构造器还是 Run,都可选择传递一个
CancellationToken
,它使 Task 能在调度前取消;
控制 Task 执行方式
还可选择向构造器传递一些 TaskCreationOptions
标志来控制 Task 的执行方式,其定义了一组可按位 OR 的标志;
4.2 Task 基本操作
4.2.1 获得 Task 执行结果
使用 Task<TResult>
对象获得结果
当需要获得当前传入的方法在 Task 中的执行结果时,可以使用 Task 的泛型模式:Task<TResult>
- 参数
- 参数表示当前计算限制操作的返回类型;
- 方法
task.Wait()
- 调用了 task 对象的
Wait()
方法后,将可以显式的等待任务完成;
代码示例
Task<Int32> t = new Task<Int32>( n => Sum((Int32)n,1000000));
//也可以不显式Start,Wait执行后,若当前t1线程没有执行,Wait将会进行Start操作;
t.Start();
//等待任务完成
//若Task正在执行,则将会阻塞当前t.Wait()的调用线程;
t.Wait();
4.2.2 取消任务
当需要显式的控制一个任务的取消时,可以使用
CancellationTokenSource
来进行;
什么是 CancellationTokenSource CancellationToken 提供了用于控制和检查任务是否取消的操作。想要让一个 Task 接受 CancellationToken ,需要让任务接受的执行的函数当中用于 CancellationToken 作为参数;
- `CancellationToken和
- ThrowIfCancellationRequested():此方法用于定时检查操作是否已取消;比如在以下的 Loop 被进行使用;
任务使用方法
private static int32 Sum(CancellationToken ct,int32 n)
{
Int32 sum = 0;
for(;n>0;n--)
{
ct.IsCancellationRequested();//
checked(sum+=n;);
}
}
创建 CancellationTokenSource 与 Task
CancellationTokenSource cts = new CancellationTokenSource ();
Task<Int32> t = Task.Run()
cts.Cancel();//这行代码将会后续的某个时间被使用后,
4.2.3 ContinueWith 与新任务
什么是自动启动新任务 用于检测一个任务在什么时候结束运行,进而在此任务完成后启动以一个任务
- 可以使用
Task cwt = t.ContinueWith(//...);
此方法
cwt 的作用 cwt 获得的 task 的任务执行完成后将可以启动另一个任务,并且不会进入阻塞状态; 注意:如果线程本身是一个线程池线程,那么它可以返回池中以执行其他操作;
TaskContinuationOptions 枚举
- 可在调用 ContinueWith 时传递对一组 TaskContinuationOptions 枚举值,用于指定新任务的执行方式;
- 示例:
t.ContinueWith(task => Console.Write("Hello"),TaskContinuationOptions.OnlyOnRanToCompletion)
- 示例:
4.2.4 父、子任务
什么是子任务 在使用 Task 时,一个 Task 可以作为父任务,为其创建多个 Task 作为子任务;
代码示例:为父任务创建三个子任务
Task<Int32[]> parent = new Task<Int32[]>(() => {
var results = new Int32[3];
// 在parent这个所属的Task中创建的三个任务其实默认不属于parent,而是顶级任务
// 因为在创建Task时指定了TaskCreationOptions.AttachedToParent的标志,因此创建的所有子任务将和创建它的任务产生关联
new Task(() => results[0] = Sum(1000),TaskCreationOptions.AttachedToParent).Start();
new Task(() => results[0] = Sum(2000),TaskCreationOptions.AttachedToParent).Start();
new Task(() => results[0] = Sum(3000),TaskCreationOptions.AttachedToParent).Start();
return results;
})
//用4.2.3的方法创建新任务,用于显示结果
var cwt = parent.ContinueWith(
parentTask => Array.ForEach(parentTask.Resule,Console.Write);
);
parent.Start();
4.3 任务工厂与调度器
4.3.1 任务工厂
为什么需要任务工厂
- 目的
- 有时需要创建一组共享相同配置的 Task 对象;
- 为避免机械地将相同的参数传给每个 Task 的构造函数,可以创建一个任务工厂,来封装这些 Task 的通用的配置;
什么是任务工厂
- TaskFactory
- 在
System. Threading. Tasks
命名空间定义了一个TaskFactory
类型和一个TaskFactory<TResul>
类型; - 两个类型都直接继承自
System. Object
;
- 在
TaskFactory
类- 用于构造一个返回 void 的任务工厂;
TaskFactory<TResult>
类- 用于构造一组特定返回值类型的任务工厂;
- 通过 TResult 实参传递任务的返回类型;
创建任务工厂
var cts = new CancellationTokenSource();
//创建一个任务工厂
var tf = new TaskFactory<Int32>(
cts.Token,
TaskCreationOptions.AttachedToParent,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default
);
//任务工厂中的三个任务被加入到tf当中
var childTasks = new[]
(
tf.StartNew(() => Sum(cts.Token,10000)),
tf.StartNew(() => Sum(cts.Token,20000)),
tf.StartNew(() => Sum(cts.Token,30000)),
)
- 效果
- 每个 Task 对象都共享相同的
CancellationTokenSource
标记,任务都被视为其父任务的子任务; - TaskFactory 创建的所有延续任务都以同步方式执行,而且 TaskFactory 创建的所有 Task 对象都使用默认
TaskScheduler
; - 使用 StartNew 可以很方便的创建并启动新的子任务;
- 每个 Task 对象都共享相同的
4.3.2 调度器
什么是调度器 TaskScheduler 对象负责执行被调度的任务,同时向 Visual Studio 调试器公开任务信息;
- FCL 提供了两个派生自 TaskScheduler 的类型
- 线程池任务调度器(thread pool task scheduler)
- 线程池任务调度器将任务调度给线程池的工作者线程;
- 同步上下文任务调度器(synchronizationcontext taskscheduler)
- 同步上下文任务调度器适合提供了图形用户界面的应用程序;
- 将所有任务都调度给应用程序的 GUI 线程,使所有任务代码都能成功更新 UI 组件;
- 线程池任务调度器(thread pool task scheduler)
- 默认情况下,所有应用程序使用的都是线程池任务调度器;
基础使用 获得同步上下文任务调度器的引用:
TaskScheduler m_syncContextTaskScheduler;
m_syncContextTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
任务使用同步上下文任务调度器:
//t是一个任务
t.ContinueWith
(
task => Text = "Operation Canceled",
CancellationToken.None,
TaskContinuationOptions.OnlyOnCanceled,
m_syncContextTaskScheduler,
);