零基础快速入门C#并发编程:计算限制的异步操作(2) -- Task 任务

70 阅读7分钟
  • 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 对象都有一组字段,这些字段构成了任务的状态;
  • 其中包括
      1. Int32 的 ID
      1. 代表 Task 执行状态的一个 Int32 :Status
      1. 对父任务的引用
      1. 对 Task 创建时指定的 TaskScheduler 的引用
      1. 对回调方法的引用
      1. 对要传给回调方法的对象的引用
      1. 对 ExecutionContext 的引用
      1. 对 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 出错

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,都可选择传递一个 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 可以很方便的创建并启动新的子任务;

4.3.2 调度器

什么是调度器 TaskScheduler 对象负责执行被调度的任务,同时向 Visual Studio 调试器公开任务信息;

  • FCL 提供了两个派生自 TaskScheduler 的类型
    • 线程池任务调度器(thread pool task scheduler)
      • 线程池任务调度器将任务调度给线程池的工作者线程;
    • 同步上下文任务调度器(synchronizationcontext taskscheduler)
      • 同步上下文任务调度器适合提供了图形用户界面的应用程序;
      • 将所有任务都调度给应用程序的 GUI 线程,使所有任务代码都能成功更新 UI 组件;
  • 默认情况下,所有应用程序使用的都是线程池任务调度器;

基础使用 获得同步上下文任务调度器的引用:

TaskScheduler m_syncContextTaskScheduler;
m_syncContextTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();

任务使用同步上下文任务调度器:

//t是一个任务
t.ContinueWith
(
	task => Text = "Operation Canceled",
	CancellationToken.None,
	TaskContinuationOptions.OnlyOnCanceled,
	m_syncContextTaskScheduler,
);