多线程4:使用任务并行库

1、任务并行库
.Net4.0引入了任务并行库(Task Parallel Library,简称TPL),.Net4.5对其进行了轻微的改进,使用更简单。
TPL可认为是线程池之上的又一抽象层,隐藏了与线程池交互的底层代码,提供了更方便的细粒度API。
TPL的核心是任务,即异步操作,该操作可通过多种方式运行,可以使用或者不使用独立线程运行。
TPL的关键优势是具有用于组合任务的便利的API。
使用AggregateException可以捕获底层任务内部的所有异常,并允许单独处理这些异常。

2、创建任务
下面3种方式可以创建任务:
- 使用Task.Start方法
- Task.Run
- Task.Factory.StartNew
使用Task对象创建任务,需要调用Start方法才能启动任务。
Task.Run是Task.Factory.StartNew的一个特例,包含DenyChildAttach特性,即阻止子任务附加到其父任务。
推荐使用Task.Run创建并启动任务,而非Task.Factory.StartNew。
使用Task.Factory.StartNew方法创建的任务标记为长时间运行(TaskCreationOptions.LongRunning)时,该任务不会使用线程池。

3、使用任务执行基本的操作
使用Task.Start方法启动任务后,调用Task.Result属性会阻塞当前线程,直到任务完成并返回结果。
Task.RunSynchronously方法会使任务运行在当前线程中,可以避免非常短暂的操作使用线程池。
Task.Status属性表示任务的状态,有Created、Running、RanToCompletion等,可通过轮询该状态以判断任务是否完成。

4、组合任务
通过Task.ContinueWith方法可以为指定任务设置后续任务,后续任务会在指定的任务完成后运行。
后续任务会从线程池中获取新的工作线程,然后运行。
通过TaskContinuationOptions.ExecuteSynchronously选项可以为后续任务指定同步操作:
- 若前置任务未完成,则后续任务在前置任务所在线程运行;
- 若前置任务已完成,则后续任务在当前线程运行。
Task..GetAwaiter().OnCompleted方法会在任务结束后执行:
- 若前置任务未完成,则该方法在前置任务所在线程运行;
- 若前置任务已完成,则该方法在线程池中获取新的工作线程中运行。
TaskCreationOptions.AttachedToParent选项可以为指定任务附加子任务:
- 子任务必须在父任务运行时创建;
- 可以为子任务继续附加子任务;
- 父任务完成后等待子任务的状态为WaitingForChildrenToComplete;
- 所有的子任务都结束后,父任务才会完成。

5、将APM模式转换为任务
使用Task<T>.Factory.FromAsync方法可以将APM模式转换为任务。
(1) Task<T>.Factory.FromAsync(IAsyncResult asyncResult, Func<IAsyncResult, TResult> endMethod);

var task = Task<string>.Factory.FromAsync(
    asyncTask.BeginInvoke("AsyncTaskThread", Callback, "a delegate asynchronous call"),
    asyncTask.EndInvoke
);

(2) Task<T>.Factory.FromAsync(

Func<TArg1, AsyncCallback, object, IAsyncResult> beginMethod,
Func<IAsyncResult, TResult> endMethod,
TArg1 arg1, object state); 

var task = Task<string>.Factory.FromAsync(
    asyncTask.BeginInvoke,
    asyncTask.EndInvoke,
    "AsyncTaskThread",
    "a delegate asynchronous call"
);

(3) Task<T>.Factory.FromAsync(IAsyncResult asyncResult, Func<IAsyncResult, TResult> endMethod);

var ar = inAsyncTask.BeginInvoke(out var threadId, Callback, "a delegate asynchronous call");
task = Task<string>.Factory.FromAsync(ar, _ => inAsyncTask.EndInvoke(out threadId, ar));

6、将EAP模式转换为任务

使用TaskCompletionSource<T>类型可以将EAP模式转换为任务。
针对EAP模式结束回调的不同情况,将Error、Cancelled、Result等设置给TaskCompletionSource对象。
通过TaskCompletionSource.Task.Result获取结果。
需要注意的是,要将tcs.SetResult方法封装在try/catch代码块中,以保证错误信息会设置给任务完成源对象。
也可以使用tcs.TrySetResult方法设置操作结果。

7、实现取消选项
使用CancellationTokenSource和CancellationToken可以实现任务的取消。
如果在任务启动前取消任务,任务状态为Canceled,再次调用Start方法会抛出InvalidOperationException异常。
如果在任务启动后取消任务,任务状态为RanToCompletion,表明任务已完成(尽管是取消以后完成)。

8、任务中的异常
(1) 对启动的任务直接获取Result属性,会使当前线程等待直到任务完成,并将异常传播给当前线程。
(2) 聚合异常AggregateException通常封装了线程内部的异常,可通过InnerException属性获取内部异常。
(3) AggregateException异常的InnerException属性也可能是聚合异常,
可通过根聚合异常的Flatten方法的InnerExceptions获取所有子聚合异常的内部异常。
(4) Task.GetAwaiter().GetResult()方法同步等待任务执行完毕并获取结果,TPL基础设施会管理这种方法的异常,
通过对该方法进行try/catch块捕获的异常是线程原始异常,无需通过AggregateException封装。
(5) 通过ContinueWith创建后续任务时,指定TaskContinuationOptions.OnlyOnFaulted选项会在任务抛出异常时才会响应,
可通过该方式实现对Task异常的监控。

9、并行运行任务
Task.WhenAll会在所有任务都完成后创建一个任务,该任务的结果是之前所有任务的结果集合。
Task.WhenAny会等待一些任务中的任何一个完成,Task.WhenAny(tasks).Result即为最先完成的那个任务。

10、使用TaskScheduler配置任务的执行
TaskScheduler组件负责如何执行任务,默认的任务调度将任务放置到线程池的工作者线程中,TPL将其设为默认选项。
不允许从创建UI以外的线程中访问UI组件。
UI线程中使用TaskScheduler.FromCurrentSynchronizationContext()选项允许提供一个后续操作用来和UI组件交互。
通过该选项,TPL给UI线程的后续操作中放入代码,并借助UI线程消息循环来异步运行该代码。
为避免死锁,不要通过任务调度程序在UI线程中使用同步操作,可以使用ContinueWith或async/await方法。

posted @   xhubobo  阅读(62)  评论(0编辑  收藏  举报
编辑推荐:
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器
历史上的今天:
2020-05-13 类的Big-Three:构造函数、析构函数与赋值函数
点击右上角即可分享
微信分享提示