现状
使用ThreadPool的QueueUserWorkItem方法完成异步操作会存在两个问题:
1、系统无法知道异步操作是否完成
2、无法获取异步操作完成时的返回值
问题来了,那就需要新的解决方案(忽然想起上《通信原理》时老师讲的话,“遇到问题,解决问题,因此就有了不同的编码方式”,从调幅,到调频,再到码分….,工程领域的主题就是遇到问题,解决问题!跑题了!)
为了解决上面提到的问题,.NET提出了Task的概念
Task
Task的构造方法如下图所示:
其中参数有:
Action:.NET内部委托
Object:传入操作的数值
CancellationToken:协作式取消的Token
TaskCreationOption:创建Task时的创建方式选项
对于第四项,其规定TaskScheduler的行为方式,让TaskScheduler按照规定的方式创建Task。但是,TaskScheduler不一定按照此方式执行。因此这个参数不是很重要。
对于Task还有一个特别的地方:Task所使用的线程并不是来自.NET的线程池,而是新创建的一个Thread
可返回结果的Task类
可以使Task返回结果,这个时候需要用到Task<TResult>
类,其就是代表一个可以有返回值的异步操作。
本类的构造函数如下:
其中,Func(TResult)的定义如下:
public delegate TResult Func<out TResult>()
具体的例子如下:
假设,操作的代码如下:
private static int Sum(int n)
{
int sum=0;
for(;n>0;n--)
checked{sum += n;}
return sum;
}
若程序希望获取操作的返回值,则应该如此使用:
Task<int> t = new Task<int>(n=>Sum((int)n),1000000);
t.start();
t.wait(); //可以写这句,也可以不写
Console.WriteLine(t.Result);
在.NET中有两个Task类,一个是不返回参数的,其参数中使用的是Action委托
可返回结果的Task,其参数是使用的Func<Object,TResult>
或者是不带参数的Func<TResult>
Task的异常
在Task的异步操作中可能会出现异常,若是使用Task<TResult>
,在调用其Wait或Result方法时,异常就会抛出。若使用的Task
,则可以调用wait方法,异常就会抛出。另外,不管是Task<TResult>
还是Task
,只要查询其Exception属性,异常也会抛出。
Task的异常会被存储在一个异常集合中,其名称为AggregateException。其内部有一个InnerExceptions 的属性,其定义如下:
public ReadOnlyCollection<Exception> InnerExceptions { get; }
通过定义可以看到其返回结果是一个异常的集合类。因此,可以通过for循环,完成对每一个异常的处理
AggregateException还有一个Handle方法,该方法的定义如下:
public void Handle(Func<Exception,?bool> predicate)
(写到这儿,不得不发出感慨!当初设计.NET的这帮人真他妈厉害!)
这个方法的作用就是为AggregateException内包含的每一个异常都调用一个回调方法
举例说明:
using System;
using System.IO;
using System.Threading.Tasks;
public class Example
{
public static void Main()
{
// This should throw an UnauthorizedAccessException.
try {
var files = GetAllFiles(@"C:\");
if (files != null)
foreach (var file in files)
Console.WriteLine(file);
}
catch (AggregateException ae) {
//这儿就是一个遍历InnerExceptions循环,将内部所有的异常全部遍历,然后进行相应处理
//下面的代码比较简单,可通过判断是个什么异常,从而进行下一步的相关操作
foreach (var ex in ae.InnerExceptions)
//例如:
//if(ex is UnauthorizedAccessException)
// doSomeThing();
Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message);
}
Console.WriteLine();
// This should throw an ArgumentException.
try {
foreach (var s in GetAllFiles(""))
Console.WriteLine(s);
}
catch (AggregateException ae) {
foreach (var ex in ae.InnerExceptions)
Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message);
}
}
static string[] GetAllFiles(string path)
{
var task1 = Task.Run( () => Directory.GetFiles(path, "*.txt",
SearchOption.AllDirectories));
try {
return task1.Result;
}
catch (AggregateException ae) {
//Handle是传入一个异常参数,返回一个bool的结果,若处理则返回true,否则返回false
//Handle方法应该是一个遍历方法,即通过InnerExceptions属性,为每一个异常添加这个回调方法
ae.Handle( x => { // Handle an UnauthorizedAccessException
if (x is UnauthorizedAccessException) {
Console.WriteLine("You do not have permission to access all folders in this path.");
Console.WriteLine("See your network administrator or try another path.");
}
return x is UnauthorizedAccessException;
});
return Array.Empty<String>();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
Task的方法关于阻塞
Task类中有几个Wait方法:Wait、WaitAny、WaitAll。具体可参考MSDN的方法说明:具体的链接在这儿,这儿说明的是:
不管是哪个方法,调用这三者方法的任何一个,都会造成调用线程被阻塞,即等待task完成相关的操作。
这儿就有一个区别,之前join方法与wait方法同样都是让线程等待,但是内部如何实现及两者的区别
.NET中join与wait方法的区别
在.NET中join方法的源码如下:(查看.NET的源码的网址在这儿:查看.NET的源码网址)
//Join方法的源代码
[SecuritySafeCritical, HostProtection(SecurityAction.LinkDemand, Synchronization=true, ExternalThreading=true)]
public void Join()
{
this.JoinInternal();
}
JoinInternal的源代码未知,只知是window内核的方法
[MethodImpl(MethodImplOptions.InternalCall), SecurityCritical, HostProtection(SecurityAction.LinkDemand, Synchronization=true, ExternalThreading=true)]
private extern void JoinInternal();
而wait方法的代码如下:
//其他的无关代码,关键的一行如下:
Thread.SpinWait(Environment.ProcessorCount * (((int) 4) << i));
而SpinWait是也是window内核的方法,但是通过名称可以得知,其是一个自旋的等待。即代表占用CPU的资源,使得线程被挂起。
个人对join方法的理解,如下图所示:
如上图所示,当程序启动后,主线程会执行,如图中1所示,声明一个task后,task进行异步相关操作,如图2所示。当主线程到达t1.join()时,主线程机会挂起(即主线程不释放资源或同步锁,进入等待状态,此处不确定是用户模式等待还是内核模式等待)(如图3),然后task线程执行需要完成的操作(如图4),直到完成(如图5)。然后主线程才会执行剩余的内容(如图6)所示。
个人对wait方法的理解。
暂未找到.NET中关于wait方法的详细说明,因此有两个疑问:
1、调用线程(the calling thread)在阻塞时,是否释放同步锁[此处个人认为是不释放同步锁]
2、wait方法与join方法的差异
另外,wait方法很有意思,当调用线程遇到t.Wait()
语句时, 调用线程并不是就傻傻的被阻塞了,而是先去看看当前线程是否开始执行,若当前线程尚未开始执行操作,则调用线程就会将当前线程将要执行的操作加载到调用线程中执行;若当前线程的操作已经开始执行了,那没办法,调用线程只能被阻塞。
MSDN上有一篇文章wait方法执行逻辑进行了详细说明,具体的网址在这儿。
取消任务
取消任务是指使用协作式取消方式取消task。这个可以根据task的构造器,传入一个token。举例如下:
//构造一个取消操作对象
private CancellationTokenSource cts = new CancellationSource();
public static void Main(string[] args)
{
//定义一个具有返回结果类型的Task<TResult>
Task<int> t=new Task<int>(tempMethod,t.Token);
t.start();
//取消操作
cts.Cancel();
try
{
Console.WriteLine("返执行结果是:"+t.Result);
}
catch(AggregateException ex) //只要是使用了Task,则catch捕获的异常就应该使用AggregatException,而不是简单的使用Exception
{
ex.Handle(e=>e is OperationCanceledException);
Console.WriteLine("Sum操作已经完成")
}
}
//要符合Func委托
private static int tempMethod()
{
int resultInt=Sum(cts.Token,10000);
}
private static int Sum(CancellationToken ct,int n)
{
int sum=0;
for(;n>0;n--)
{
//若请求操作取消操作,则抛出OperationCanceledException
ct.ThrowIfCancellationRequest();
checked{sum+=n;}
}
return sum;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
当任务完成后,自动执行下一个任务
通过wait方法或者是task.Result属性获取操作最后的结果,这个是存在问题的。因为这样做会阻塞线程(Result属性的内部就是调用了wait方法)。因此为了避免因阻塞而导致的性能问题,.NET提供了一种回调机制,当线程完成操作时,就会调用callback方法。
特别说明,调用callback的线程是一个新的线程。
而实现callback的就是ContinueWith方法,ContinueWith方法的定义如下,更多的定义参见:MSDN中方法的定义
public Task ContinueWith(
Action<Task, object> continuationAction,
object state,
CancellationToken cancellationToken,
TaskContinuationOptions continuationOptions,
TaskScheduler scheduler
)
2、一个Task对象内部包含了一个ContinueWith的对象集合。即一个task对象可以声明多个继续的操作。例如:
Task t = new Task(Action());
t.ContinueWith(task=>Console.WriteLine("操作1:Task的结果是"+t.Result));
t.ContinueWith(task=>Console.WriteLine("操作2:Task的结果是"+t.Result));
t.ContinueWith(task=>Console.WriteLine("操作3:Task的结果是"+t.Result));
当Task的操作完成后,线程池中会启用3个线程处理相应的回调方法。
3、ContinueWith方法中有一参数:TaskContinuationOptions,是指存在某些情况的下才调用,具体参见参数。一般而言,continuewith是通过创建一个独立的task完成回调方法的调用,但是这个选项中也可以有一个AttachedToParent选项,使其成为一个Task的子任务
创建子任务
因为task的构造器中只是定义了委托的定义,但没有规定符合委托的方法中是否可以创建task。因此可以在task内部创建子任务,但是在创建子任务的时候,需要明确子任务的创建方式,若是正常创建,则新任务将是独立的task。想成为task的子任务,可以在task的构造器中使用TaskCreationOptions.AttachedToParent属性。这样就相当于子任务与父任务绑定在一起,只有当所有的任务完成时,父任务才认为是完成了。举例如下:
//在定义的时候同步创建了3个子线程,并且用了TaskCreationOptions.AttachedToParent属性
Task<int[]> parent = new Task<int[]>(
int[] results=new int[3];
new Task(()=>results[0]=Sum(100),TaskCreationOptions.AttachedToParent).start();
new Task(()=>results[0]=Sum(200),TaskCreationOptions.AttachedToParent).start();
new Task(()=>results[0]=Sum(300),TaskCreationOptions.AttachedToParent).start();
return results;
);
var ctw = parent.ContinueWith(
parentTask=> Array.ForEach(Console.WriteLine("结果为:"+parentTask.Result));
);
parent.Start();
任务内部/小节
1、Task所使用的线程默认是创建新线程,而不是使用线程池线程。因此,其资源的占用和消耗要大于ThreadPool.QueueUserWorkItem
2、Task实现了IDispose接口,因此在用完task时要调用dispose,以释放资源。而不要使用GC回收
3、每个Task有唯一ID,可通过CurrentID获取,而CurrentID是一个可控类型的Int32
4、Task在生命周期中会有几个状态:
4.1 Createed //任务显示的创建完。可使用start方法,手动开始这个任务
4.2 WaitingForActivation //任务隐式创建。会自动开始。例如,通过continuewith开始的任务
4.3 WaitingToRun //已经进入调度,尚未开始
4.4 Runing
4.5 WaitingForChildrenToComplete
4.6 task最终的结果为:
4.6.1 RanToCompletion
4.6.2 Cancelled
4.6.3 Faulted
5、可以通过task的Status属性获取task的状态。同时Task提供了几个属性:
IsCanceled、IsCompleted、IsFaulted判断task的状态。但是有一个特殊情况:当Task的状态为RanToCompletion、Cancelled、Faulted中的任意状态时,调用Task的IsCompleted属性,其都将返回true。因此判断一个task是正常的完成的方式是:
if(task.Status==TaskStatus.RanToCompletion)
{
//.....
}
任务工厂
有时候会遇到一种情况,用相同的配置创建多个Task。一种方法是挨个创建task;而另外一个方法就是使用任务工厂。(个人认为任务工厂使用的机会不会很多)
任务工厂同Task,也有两种方式,TaskFactory、TaskFactory<TResult>
。
可以通过TaskFactory的构造器创建TaskFactory实例,也可以使用Task类的TaskFactory属性进行创建,一般情况是使用后者。
TaskFactory具体的使用方法,可以参考MSDN的资料:TaskFactory
任务调度器
1、.NET中有多个类型的任务调度器,其中线程池使用的是“线程池线程任务调度器”,而GUI(WinForm、WPF、SilverLight)使用的则是“同步上下文任务调度器”,而通过不同的任务调度器所创建的task,其不允许相互操作。因此使用“线程池线程任务调度器”所创建的线程,不能操作界面(改变标题之类的操作),否则就会爆出InvalidOperationException
但是若希望线程池中的操作可以操作GUI上的元素,则其需通过TaskScheduler.FromCurrentSynchronizationContext()方法获得“同步上下文任务调度器”,在创建task时,将其作为参数传入。书中的例子很好,如下:
internal sealed class MyFrom:Form
{
//Form的构造器,初始化标题等内容
public MyFrom(){
this.text="同步上下文任务调度器demo";
visible=true;
width=400;
height=400;
}
//通过TaskScheduler获得当前Form的“同步上下文任务调度器”
private readonly TaskScheduler m_syncContextTaskScheduler=
TaskScheduler.FromCurrentSynchronizationContext();
private CancellationTokeSource cts;
//重写鼠标的点击方法
protected override void OnMouseClick(MouseEventArgs e){
if(cts!=null){
cts.cancel();
cts=null;
}else{
text="操作开始";
cts = new CancellationTokeSource();
//下面的这个task使用线程池的任务调度器,也就是默认的任务调度器
var task = new task(()=>Sum(cts.Token,2000),cts.Token);
task.ContinueWith(
t=>Text="结果为"+t.Result,
CancellationToken.None,
TaskContinuationOptions.OnlyOnRanToCompletion,
syncContextTaskScheduler
);
task.ContinueWith(
t=>Text="操作被取消",
CancellationToken.None,
TaskContinuationOptions.OnlyOnCanceled,
syncContextTaskScheduler
);
task.ContinueWith(
t=>Text="操作失败",
CancellationToken.None,
TaskContinuationOptions.OnlyOnFaulted,
syncContextTaskScheduler
);
}//else结束
base.OnMouseClick(e);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
代码简单易懂,有一点很有意思。就是传入的参数中有:CancellationToken.None
,这个很有意思。其本质的想法就是,我创建的这个操作,不想被外界的cancel方法所取消,但是方法中还必须有一个这个参数,因此就使用CancellationToken.None
。这个CancellationToken.None
会返回一个CancellationToken,因其与任何的CancellationTokenSource没有任何关系,因此操作也就不能被取消了
26.4读书思考
1、task还是挺耗费资源的,但是又比Thread、ThreadPool等好用,没办法只能使用它
2、在26.4遇到一个问题,join与wait方法区别,有待解答!