任务(Task)是一个管理并行工作单元的轻量级对象。它通过使用CLR的线程池来避免启动专用线程,可以更有效率的利用线程池。System.Threading.Tasks 命名空间下任务相关类一览:
类 | 作用 |
---|---|
Task | 管理工作单元 |
Task | 管理带返回值的工作单元 |
TaskFactory | 创建任务 |
TaskFactory | 创建任务或者有相同返回值的延续任务 |
TaskScheduler | 管理任务调度 |
TaskCompletionSource | 手动控制任务工作流 |
任务用来并行地执行工作,充分地利用多核:事实上,Parallel和PLINQ内部就是建立在任务并行的结构上。
任务提供了一系列强大的特性来管理工作单元,包括:
协调任务调度
建立一个任务从另一个任务中启动的父子关系
实现合作取消(cooperative cancellation)模式
无信号的任务等待
附加延续任务(continuation)
基于多个祖先任务调度一个延续任务
传递异常到父任务、延续任务或任务消费者
同时任务实现了一个本地工作队列,它允许你高效地创建快速执行的子任务而不用遭受在单个工作队列时的竞争花费。任务并行库让你用最小的花费来创建成百上千的任务,但是如果你想创建上百万个任务,就必须分割这些任务到更大的工作单元,以保持效率。
创建与启动任务
有两种方法可以创建任务,一种是通过TaskFactory的StartNew()方法创建并启动任务;另一种是调用Task构造函数创建,然后手动启动任务。需要注意的是,任务启动后并不会立即执行,它是由任务调度器(TaskScheduler)来管理的。
TaskFactory的StartNew()方法创建任务的示例如下:
//没有返回值
Task.Factory.StartNew(() => Console.WriteLine("Task Created!"));
//有返回值
var task = Task.Factory.StartNew<string>(() => "Task Created!");
Console.WriteLine(task.Result);
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
调用Start方法手动启动的示例如下:
var task = new Task<string>(() => "Task Created!");
task.Start();//异步执行
Console.WriteLine(task.Result);
- 1
- 2
- 3
- 1
- 2
- 3
调用RunSynchronously方法手动启动的示例如下:
var task = new Task<string>(() => "Task Created!");
task.RunSynchronously();//同步执行
Console.WriteLine(task.Result);
- 1
- 2
- 3
- 1
- 2
- 3
也可以在创建任务时指定一个任务状态参数,可以通过任务的AsyncState属性来访问该参数。示例:
var task = Task.Factory.StartNew(state => "hello " + state, "Mike");
Console.WriteLine(task.AsyncState);
Console.WriteLine(task.Result);
- 1
- 2
- 3
- 1
- 2
- 3
你还可以指定一个任务创建选项(TaskCreationOptions) ,这个枚举类型有以下枚举值:None,LongRunning,PreferFairness,AttachedToParent。下面解释各个枚举值的作用。
- LongRunning:顾名思义就是长时间运行的任务,此选项建议任务调度器分配一个专用的线程给任务。这样做的原因是:长时间运行的任务可能会阻塞任务队列,导致那些短小的任务一直得不到执行。LongRunning也适合那些阻塞的任务。
- PreferFairness:公平第一,此选项建议任务调度器尽量按照任务的启动时间来调度任务。但是它通常可能不这样做,因为它使用本地工作偷取队列(local work-stealing queues)优化任务调度。这个优化对那些非常小的任务很有用。
- AttachToParent:附加到父任务,此选项用来创建子任务。创建子任务示例:
//第一种方式:
var parent = Task.Factory.StartNew(() =>
{
var nonChildTask = Task.Factory.StartNew(
() => Console.WriteLine("I'm not a child task.")
);
var childTask = Task.Factory.StartNew(
() => Console.WriteLine("I'm a child task."),
TaskCreationOptions.AttachedToParent);
});
//第二种方式:
Task parent=new Task(()=>
{
DoStep1();
});
Task task2 = parent.ContinueWith ((PrevTask) =>
{
DoStep2();
});
parent.Start();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
任务等待
任务可以通过Wait()成员方法或Result属性来等待任务完成。
当调用Result属性时,将会执行下列操作:
1.如果任务已结束,返回任务结果
2.如果任务已开始,等待任务结束
3.如果任务尚未开始执行,则在当前线程执行任务
Task.WaitAny()静态方法等待任何一个任务完成。示例:
var tasks = new Task[3];
for (int i = 0; i < tasks.Length; i++)
{
int taskIndex = i;
tasks[i] = Task.Factory.StartNew(() =>
{
int seed=Guid.NewGuid().GetHashCode();
int waitTime = new Random(seed).Next(10, 100);
Thread.Sleep(waitTime);
Console.WriteLine("Task{0} Finished", taskIndex);
});
}
Task.WaitAny(tasks);
Console.WriteLine("已有任务完成");
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
Task.WaitAll()静态方法等待所有任务完成。即使有任务抛出异常也不会终止等待,它会在所有任务完成之后抛出一个AggregateException异常,这个异常聚合了所有任务抛出的异常。示例:
var tasks = new Task[3];
for (int i = 0; i < tasks.Length; i++)
{
int taskIndex = i;
tasks[i] = Task.Factory.StartNew(() =>
{
int waitTime = new Random(Guid.NewGuid().GetHashCode()).Next(10, 100);
Thread.Sleep(waitTime);
Console.WriteLine("Task{0} Finished", taskIndex);
});
}
Task.WaitAll(tasks);
Console.WriteLine("所有任务完成");
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
异常处理
默认情况下任务未处理的异常会终止应用程序。需要指出的是任务中未处理的异常不会立即导致应用程序终止,异常要延迟到垃圾回收器回收任务并调用Finalize方法时才会终止程序。如果读取了任务的Exception属性,这个操作将阻止随后的应用程序终止。
当等待任务完成时,所有未处理的异常会传递到调用方。
Wait()方法超时的异常也必须处理,否则导致应用程序终止。
子任务中未处理的异常会冒泡传递到父任务;嵌套任务中的非子任务的异常不会传递到这个任务的上一层任务,需要单独处理,否则将导致应用程序终止。
var task = Task.Factory.StartNew(() =>
{
Task.Factory.StartNew(() => { throw null; }, TaskCreationOptions.AttachedToParent);
Task.Factory.StartNew(() => { throw null; }, TaskCreationOptions.AttachedToParent);
Task.Factory.StartNew(() => { throw null; }, TaskCreationOptions.AttachedToParent);
});
task.Wait();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
TaskScheduler.UnobservedTaskException静态事件提供了最后一种手段处理所有未处理异常。通过处理这个事件,就不用终止应用程序,而用你自己的异常处理逻辑替代它。
取消任务
当创建任务时,可以传入一个取消令牌(CancelationToken)参数,这样就可以安全的取消任务。示例:
var source = new CancellationTokenSource();
var token = source.Token;
var task = Task.Factory.StartNew(() =>
{
Console.WriteLine("Task starting...");
while (true)
{
token.ThrowIfCancellationRequested();
Console.WriteLine("I'm alive. {0}",DateTime.Now);
Thread.Sleep(1000);
}
},token);
Task.Factory.StartNew(() =>
{
Thread.Sleep(4500);
source.Cancel();
});
try
{
task.Wait();
Console.WriteLine("Task stopped.");
}
catch (AggregateException e)
{
if (e.InnerException is OperationCanceledException)
{
Console.WriteLine("Task canceled.");
}
else
{
Console.WriteLine("errors.");
}
}
- 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
- 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
通过调用CancellationTokenSource的Cancel()方法取消任务,这个并不会立即终止任务,一直延迟到任务下次检测是否取消时才通过抛出OperationCanceledException终止任务。
如果想通过直接抛出OperationCanceledException异常的方式取消任务,则需要在任务中传入CancelationToken参数,否则就不能将任务的状态为TaskStatus.Canceled并触发OnlyOnCanceled延续任务。
此外,取消令牌也可以传递到Wait和CancelAndWait方法中来取消等待。
随着 .NET 4.0的到来,她与以前各版本的一个明显差别就是并行功能的增强,以此来适应这个多核的世界。于是引入了一个新概念—任务,作为支持并行运算的重要组成部分,同时,也作为对线程池的一个补充和完善。从所周知,使用线程池有两个明显的缺点,那就是一旦把我们要执行的任务放进去后,什么时候执行完成,以及执行完成后需要返回值,我们都无法通过内置的方式而得知。由于任务(Task)的推出,使得我们对并行编程变得简单,而且不用关心底层是怎么实现的,由于比线程池更灵活,如果能掌握好Task,对于写出高效的并行代码非常有帮助。
一、新建任务
在System.Threading.Tasks命名空间下,有两个新类,Task及其泛型版本Task,这两个类是用来创建任务的,如果执行的代码不需要返回值,请使用Task,若需要返回值,请使用Task。
创建任务的方式有两种,一种是通过Task.Factory.StartNew方法来创建一个新任务,如:
Task task = Task.Facotry.StartNew(()=>Console.WriteLine(“Hello, World!”));//此行代码执行后,任务就开始执行
- 1
- 1
另一种方法是通过Task类的构造函数来创建一个新任务,如:
Task task = new Task(()=>Console.WriteLine(“Hello, World!”));//此处只把要完成的工作交给任务,但任务并未开始
task.Start();//调用Start方法后,任务才会在将来某个时候开始执行。
- 1
- 2
- 3
- 1
- 2
- 3
同时,我们可以调用Wait方法来等待任务的完成或者调用IsCompleted属性来判断任务是否完成。需要说明的是,两种创建任务的方法都可以配合TaskCreationOptions枚举来实现我们对任务执行的行为具体控制, 同时,这两种创建方式允许我们传递一个TaskCreationOptions对象来取消正在运行中的任务,请看任务的取消。
二、任务的取消
这世界唯一不变的就是变化,当外部条件发生变化时,我们可能会取消正在执行的任务。对于.NET 4.0之前,.NET并未提供一个内置的解决方案来取消线程池中正在执行的代码,但在.NET 4.0中,我们有了Cooperative Cancellation模式,这使得取消正在执行的任务变得非常简单。如下所示:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace TaskDemo
{
class Program
{
static void Main()
{
CancellationTokenSource cts = new CancellationTokenSource();
Task t = new Task(() => LongRunTask(cts.Token));
t.Start();
Thread.Sleep(2000);
cts.Cancel();
Console.Read();
}
static void LongRunTask(CancellationToken token)
{
//此处方法模拟一个耗时的工作
for (int i = 0; i < 1000; i++)
{
if (!token.IsCancellationRequested)
{
Thread.Sleep(500);
Console.Write(".");
}
else
{
Console.WriteLine("任务取消");
break;
}
}
}
}
}
- 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
- 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
三、任务的异常机制
在任务执行过程中产生的未处理异常,任务会把它暂时隐藏起来,装进一个集合中。当我们调用Wait方法或者Result属性时,任务会抛出一个AggregateException
异常。我们可以通过调用AggregateException
对象的只读属性InnerExceptions来得到一个ReadOnlyCollection<Exception>
对象,它才是存储抛出异常的集合,它的第一个元素就是最初抛出的异常。同样的,AggregateException对象的InnerException
属性也会返回最初抛出的异常。
值得重视的是,由于任务的隐藏机制的特点,一旦产生异常后,如果我们不调用相应的方法或者属性查看异常,我们也无法判断是否有异常产生(Task不会主动抛出异常)。当Task对象被GC回收时,Finalize方法会查检是否有未处理的异常,如果不幸刚才好有,则Finalize方法会将此AggregateException再度抛出,如果再不幸,我们没有捕获处理这个异常,则我们的程序会立即中止运行。如果发生这样的事情,会是多么大的灾难啊!
为了避免这种不幸的发生,我们可以通过注册TaskScheduler
类的静态UnobservedTaskException
事件来处理这种未被处理的异常,避免程序的崩溃。
四、任务启动任务
任务的强大与灵活之一是,当我们完成一个任务时,可以自动开始一个新任务的执行。如下所示:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace TaskDemo
{
public class AutoTask
{
static void Main()
{
Task task = new Task(() => { Thread.Sleep(5000); Console.WriteLine("Hello,"); Thread.Sleep(5000); });
task.Start();
Task newTask = task.ContinueWith(t => Console.WriteLine("World!"));
Console.Read();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
对于ContinueWith方法,我们可以配合TaskContinuationOptions
枚举,得到更多我们想要的行为。
五、子任务
任务是支持父子关系的,即在一个任务中创建新任务。如下所示:
using System;
using System.Threading.Tasks;
namespace TaskDemo
{
class ChildTask
{
static void Main()
{
Task parant = new Task(() =>
{
new Task(() => Console.WriteLine("Hello")).Start();
new Task(() => Console.WriteLine(",")).Start();
new Task(() => Console.WriteLine("World")).Start();
new Task(() => Console.WriteLine("!")).Start();
});
parant.Start();
Console.ReadLine();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
值得注意的是,以上代码中所示的子任务的调用并不是以代码的出现先后为顺序来调用的。
六、任务工厂
在某些情况下,我们会遇到创建大量的任务,而恰好这些任务共用某个状态参数(如CancellationToken),为了避免大量的调用任务的构造器和一次又一次的参数传递,我们可以使用任务工厂来为我们处理这种大量创建工作。如下代码所示:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace TaskDemo
{
public class FactoryOfTask
{
static void Main()
{
Task parent = new Task(() =>
{
CancellationTokenSource cts = new CancellationTokenSource();
TaskFactory tf = new TaskFactory(cts.Token);
var childTask = new[]
{
tf.StartNew(()=>ConcreteTask(cts.Token)),
tf.StartNew(()=>ConcreteTask(cts.Token)),
tf.StartNew(()=>ConcreteTask(cts.Token))
};
Thread.Sleep(5000);//此处睡眠等任务开始一定时间后才取消任务
cts.Cancel();
}
);
parent.Start();//开始执行任务
Console.Read();
}
static void ConcreteTask(CancellationToken token)
{
while (true)
{
if (!token.IsCancellationRequested)
{
Thread.Sleep(500);
Console.Write(".");
}
else
{
Console.WriteLine("任务取消");
break;
}
}
}
}
}
- 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
- 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
七、任务调度程序
任务的调度通过调度程序来实现的,目前,.NET 4.0内置两种任务调度程序:线程池任务调度程序(thread pool task scheduler)和同步上下文任务调度程序(synchronization context task scheduler)。默认情况下,应用程序使用线程池任务调度程序调用线程池的工作线程来完成任务,如受计算限制的异步操作。同步上下文任务调度程序通常使用UI线程来完成与Windows Forms,Windows Presentation Foundation(WPF)以及SilverLight应用程序相关的任务。
可喜的是,.NET 4.0 提供了TaskScheduler抽象类供开发人员继承来实现自定义任务调度程序的开发,有兴趣的同学可以试试。