thread task parallel plinq async await多线程 任务及异步编程
Task async的由来
async 和 await 出现在C# 5.0之后,给并行编程带来了不少的方便,特别是当在MVC中的Action也变成async之后,有点开始什么都是async的味道了。但是这也给我们编程埋下了一些隐患,有时候可能会产生一些我们自己都不知道怎么产生的Bug,特别是如果连线程基础没有理解的情况下,更不知道如何去处理了。那今天我们就来好好看看这两兄弟和他们的叔叔(Task)爷爷(Thread)们到底有什么区别和特点,本文将会对Thread 到 Task 再到 .NET 4.5的 async和 await,这三种方式下的并行编程作一个概括性的介绍包括:开启线程,线程结果返回,线程中止,线程中的异常处理等。
创建
static void Main(){
new Thread(Go).Start(); // .NET 1.0开始就有的
Task.Factory.StartNew(Go); // .NET 4.0 引入了 TPL
Task.Run(new Action(Go)); // .NET 4.5 新增了一个Run的方法
var tasks = new Action[] { () => Task1(), () => Task2(), () => Task3() };
// System.Threading.Tasks.Parallel.Invoke - 并行调用多个任务
System.Threading.Tasks.Parallel.Invoke(tasks);
}
public static void Go(){
Console.WriteLine("我是另一个线程"+Thread.CurrentThread.ManagedThreadId);
}
private static void Task1()
{
Thread.Sleep(3000);
Response.Write("Task1 - " + "ThreadId:" + Thread.CurrentThread.ManagedThreadId.ToString() + " - " + DateTime.Now.ToString("HH:mm:ss"));
Response.Write("
");
}
private static void Task2()
{
System.Threading.Thread.Sleep(3000);
Response.Write("Task2 - " + "ThreadId:" + Thread.CurrentThread.ManagedThreadId.ToString() + " - " + DateTime.Now.ToString("HH:mm:ss"));
Response.Write("
");
}
private static void Task3()
{
System.Threading.Thread.Sleep(3000);
Response.Write("Task3 - " + "ThreadId:" + Thread.CurrentThread.ManagedThreadId.ToString() + " - " + DateTime.Now.ToString("HH:mm:ss"));
Response.Write("
");
}
线程池
线程的创建是比较占用资源的一件事情,.NET 为我们提供了线程池来帮助我们创建和管理线程。Task是默认会直接使用线程池,但是Thread不会。如果我们不使用Task,又想用线程池的话,可以使用ThreadPool类。
static void Main() {
Console.WriteLine("我是主线程:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
ThreadPool.QueueUserWorkItem(Go);
Console.ReadLine();
}
public static void Go(object data) {
Console.WriteLine("我是另一个线程:Thread Id {0}",Thread.CurrentThread.ManagedThreadId);
}
传入参数
static void Main() {
new Thread(Go).Start("arg1"); // 没有匿名委托之前,我们只能这样传入一个object的参数
new Thread(delegate(){ // 有了匿名委托之后...
GoGoGo("arg1", "arg2", "arg3");
});
new Thread(() => { // 当然,还有 Lambada
GoGoGo("arg1","arg2","arg3");
}).Start();
Task.Run(() =>{ // Task能这么灵活,也是因为有了Lambda呀。
GoGoGo("arg1", "arg2", "arg3");
});
}
public static void Go(object name){
// TODO
}
public static void GoGoGo(string arg1, string arg2, string arg3){
// TODO
}
返回值
Thead是不能返回值的,但是作为更高级的Task当然要弥补一下这个功能。
static void Main() {
// GetDayOfThisWeek 运行在另外一个线程中
var dayName = Task.Run
(() => { return GetDayOfThisWeek(); });
Console.WriteLine("今天是:{0}",dayName.Result);
}
public static string GetDayOfThisWeek()
{
Console.WriteLine("doing");
Thread.Sleep(2000);
return "星期五";
}
共享数据
上面说了参数和返回值,我们来看一下线程之间共享数据的问题。
private static bool _isDone = false;
static void Main(){
new Thread(Done).Start();
new Thread(Done).Start();
}
static void Done(){
if (!_isDone) {
_isDone = true; // 第二个线程来的时候,就不会再执行了(也不是绝对的,取决于计算机的CPU数量以及当时的运行情况)
Console.WriteLine("Done");
}
}
线程之间可以通过static变量来共享数据。
线程安全
我们先把上面的代码小小的调整一下,就知道什么是线程安全了。我们把Done方法中的两句话对换了一下位置 。
private static bool _isDone = false;
static void Main(){
new Thread(Done).Start();
new Thread(Done).Start();
Console.ReadLine();
}
static void Done(){
if (!_isDone) {
Console.WriteLine("Done"); // 猜猜这里面会被执行几次?
_isDone = true;
}
}
上面这种情况不会一直发生,但是如果你运气好的话,就会中奖了。因为第一个线程还没有来得及把_isDone设置成true,第二个线程就进来了,而这不是我们想要的结果,在多个线程下,结果不是我们的预期结果,这就是线程不安全。
锁
要解决上面遇到的问题,我们就要用到锁。锁的类型有独占锁,互斥锁,以及读写锁等,我们这里就简单演示一下独占锁。
private static bool _isDone = false;
private static object _lock = new object();
static void Main(){
new Thread(Done).Start();
new Thread(Done).Start();
Console.ReadLine();
}
static void Done(){
lock (_lock){
if (!_isDone){
Console.WriteLine("Done"); // 猜猜这里面会被执行几次?
_isDone = true;
}
}
}
再我们加上锁之后,被锁住的代码在同一个时间内只允许一个线程访问,其它的线程会被阻塞,只有等到这个锁被释放之后其它的线程才能执行被锁住的代码。
Semaphore 信号量
我实在不知道这个单词应该怎么翻译,从官方的解释来看,我们可以这样理解。它可以控制对某一段代码或者对某个资源访问的线程的数量,超过这个数量之后,其它的线程就得等待,只有等现在有线程释放了之后,下面的线程才能访问。这个跟锁有相似的功能,只不过不是独占的,它允许一定数量的线程同时访问。
static SemaphoreSlim _sem = new SemaphoreSlim(3); // 我们限制能同时访问的线程数量是3
static void Main(){
for (int i = 1; i <= 5; i++) new Thread(Enter).Start(i);
Console.ReadLine();
}
static void Enter(object id){
Console.WriteLine(id + " 开始排队...");
_sem.Wait();
Console.WriteLine(id + " 开始执行!");
Thread.Sleep(1000 * (int)id);
Console.WriteLine(id + " 执行完毕,离开!");
_sem.Release();
}
在最开始的时候,前3个排队之后就立即进入执行,但是4和5,只有等到有线程退出之后才可以执行。
异常处理
其它线程的异常,主线程可以捕获到么?
public static void Main(){
try{
new Thread(Go).Start();
}
catch (Exception ex){
// 其它线程里面的异常,我们这里面是捕获不到的。
Console.WriteLine("Exception!");
}
}
static void Go() { throw null; }
那么升级了的Task呢?
public static void Main(){
try{
var task = Task.Run(() => { Go(); });
task.Wait(); // 在调用了这句话之后,主线程才能捕获task里面的异常
// 对于有返回值的Task, 我们接收了它的返回值就不需要再调用Wait方法了
// GetName 里面的异常我们也可以捕获到
var task2 = Task.Run(() => { return GetName(); });
var name = task2.Result;
}
catch (Exception ex){
Console.WriteLine("Exception!");
}
}
static void Go() { throw null; }
static string GetName() { throw null; }
一个小例子认识async & await
static void Main(string[] args){
Test(); // 这个方法其实是多余的, 本来可以直接写下面的方法
// await GetName()
// 但是由于控制台的入口方法不支持async,所有我们在入口方法里面不能 用 await
Console.WriteLine("Main Current Thread Id :{0}", Thread.CurrentThread.ManagedThreadId);
}
static async Task Test(){
// 方法打上async关键字,就可以用await调用同样打上async的方法
// await 后面的方法将在另外一个线程中执行
await GetName();
}
static async Task GetName(){
// Delay 方法来自于.net 4.5
await Task.Delay(1000); // 返回值前面加 async 之后,方法里面就可以用await了
Console.WriteLine("Current Thread Id :{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("In antoher thread.....");
}
await 不会开启新的线程,当前线程会一直往下走直到遇到真正的Async方法(比如说HttpClient.GetStringAsync),这个方法的内部会用Task.Run或者Task.Factory.StartNew 去开启线程。也就是如果方法不是.NET为我们提供的Async方法,我们需要自己创建Task,才会真正的去创建线程。
static void Main(string[] args)
{
Console.WriteLine("Main Thread Id: {0}\r\n", Thread.CurrentThread.ManagedThreadId);
Test();
Console.ReadLine();
}
static async Task Test()
{
Console.WriteLine("Before calling GetName, Thread Id: {0}\r\n", Thread.CurrentThread.ManagedThreadId);
var name = GetName(); //我们这里没有用 await,所以下面的代码可以继续执行
// 但是如果上面是 await GetName(),下面的代码就不会立即执行,输出结果就不一样了。
Console.WriteLine("End calling GetName.\r\n");
Console.WriteLine("Get result from GetName: {0}", await name);
}
static async Task GetName()
{
// 这里还是主线程
Console.WriteLine("Before calling Task.Run, current thread Id is: {0}", Thread.CurrentThread.ManagedThreadId);
return await Task.Run(() =>
{
Thread.Sleep(1000);
Console.WriteLine("'GetName' Thread Id: {0}", Thread.CurrentThread.ManagedThreadId);
return "Jesse";
});
}
进入主线程开始执行
调用async方法,返回一个Task,注意这个时候另外一个线程已经开始运行,也就是GetName里面的 Task 已经开始工作了
主线程继续往下走
第3步和第4步是同时进行的,主线程并没有挂起等待
如果另一个线程已经执行完毕,name.IsCompleted=true,主线程仍然不用挂起,直接拿结果就可以了。如果另一个线程还同有执行完毕, name.IsCompleted=false,那么主线程会挂起等待,直到返回结果为止。
只有async方法在调用前才能加await么?
static void Main(){
Test();
Console.ReadLine();
}
static async void Test(){
Task task = Task.Run(() =>{
Thread.Sleep(5000);
return "Hello World";
});
string str = await task; //5 秒之后才会执行这里
Console.WriteLine(str);
}
答案很明显:await并不是针对于async的方法,而是针对async方法所返回给我们的Task,这也是为什么所有的async方法都必须返回给我们Task。所以我们同样可以在Task前面也加上await关键字,这样做实际上是告诉编译器我需要等这个Task的返回值或者等这个Task执行完毕之后才能继续往下走。
不用await关键字,如何确认Task执行完毕了?
static void Main(){
var task = Task.Run(() =>{
return GetName();
});
task.GetAwaiter().OnCompleted(() =>{
// 2 秒之后才会执行这里
var name = task.Result;
Console.WriteLine("My name is: " + name);
});
Console.WriteLine("主线程执行完毕");
Console.ReadLine();
}
static string GetName(){
Console.WriteLine("另外一个线程在获取名称");
Thread.Sleep(2000);
return "Jesse";
}
Task.GetAwaiter()和await Task 的区别?
加上await关键字之后,后面的代码会被挂起等待,直到task执行完毕有返回值的时候才会继续向下执行,这一段时间主线程会处于挂起状态。
GetAwaiter方法会返回一个awaitable的对象(继承了INotifyCompletion.OnCompleted方法)我们只是传递了一个委托进去,等task完成了就会执行这个委托,但是并不会影响主线程,下面的代码会立即执行。这也是为什么我们结果里面第一句话会是 “主线程执行完毕”!
Task如何让主线程挂起等待?
上面的右边是属于没有挂起主线程的情况,和我们的await仍然有一点差别,那么在获取Task的结果前如何挂起主线程呢?
static void Main(){
var task = Task.Run(() =>{
return GetName();
});
var name = task.GetAwaiter().GetResult();
Console.WriteLine("My name is:{0}",name);
Console.WriteLine("主线程执行完毕");
Console.ReadLine();
}
static string GetName(){
Console.WriteLine("另外一个线程在获取名称");
Thread.Sleep(2000);
return "Jesse";
}
Task.GetAwait()方法会给我们返回一个awaitable的对象,通过调用这个对象的GetResult方法就会挂起主线程,当然也不是所有的情况都会挂起。还记得我们Task的特性么? 在一开始的时候就启动了另一个线程去执行这个Task,当我们调用它的结果的时候如果这个Task已经执行完毕,主线程是不用等待可以直接拿其结果的,如果没有执行完毕那主线程就得挂起等待了。
await 实质是在调用awaitable对象的GetResult方法
static async Task Test(){
Task task = Task.Run(() =>{
Console.WriteLine("另一个线程在运行!"); // 这句话只会被执行一次
Thread.Sleep(2000);
return "Hello World";
});
// 这里主线程会挂起等待,直到task执行完毕我们拿到返回结果
var result = task.GetAwaiter().GetResult();
// 这里不会挂起等待,因为task已经执行完了,我们可以直接拿到结果
var result2 = await task;
Console.WriteLine(str);
}
到此为止,await就真相大白了
Task使用方法
一:Task的优势
ThreadPool相比Thread来说具备了很多优势,但是ThreadPool却又存在一些使用上的不方便。比如:
1: ThreadPool不支持线程的取消、完成、失败通知等交互性操作;
2: ThreadPool不支持线程执行的先后次序; 以往,如果开发者要实现上述功能,需要完成很多额外的工作,现在,FCL中提供了一个功能更强大的概念:Task。
Task在线程池的基础上进行了优化,并提供了更多的API。在FCL4.0中,如果我们要编写多线程程序,Task显然已经优于传统的方式。
以下是一个简单的任务示例:
static void Main(string[] args)
{
Task t = new Task(() =>
{
Console.WriteLine("任务开始工作……"); //模拟工作过程
Thread.Sleep(5000);
});
t.Start();
t.ContinueWith((task) =>
{
Console.WriteLine("任务完成,完成时候的状态为:");
Console.WriteLine("IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}", task.IsCanceled, task.IsCompleted, task.IsFaulted);
});
Console.ReadKey();
}
二:Task的完成状态
任务Task有这样一些属性,让我们查询任务完成时的状态:
1: IsCanceled,因为被取消而完成;
2: IsCompleted,成功完成;
3: IsFaulted,因为发生异常而完成
需要注意的是,任务并没有提供回调事件来通知完成(像BackgroundWorker一样),它通过启用一个新任务的方式来完成类似的功能。 ContinueWith方法可以在一个任务完成的时候发起一个新任务,这种方式天然就支持了任务的完成通知:我们可以在新任务中获取原任务的结果值。 下面是一个稍微复杂一点的例子,同时支持完成通知、取消、获取任务返回值等功能:
static void Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
Task t = new Task(() => Add(cts.Token), cts.Token);
t.Start();
t.ContinueWith(TaskEnded); //等待按下任意一个键取消任务
Console.ReadKey();
cts.Cancel();
Console.ReadKey();
}
static void TaskEnded(Task task)
{
Console.WriteLine("任务完成,完成时候的状态为:");
Console.WriteLine("IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}", task.IsCanceled, task.IsCompleted, task.IsFaulted);
Console.WriteLine("任务的返回值为:{0}", task.Result);
}
static int Add(CancellationToken ct)
{
Console.WriteLine("任务开始……");
int result = 0;
while (!ct.IsCancellationRequested)
{
result++;
Thread.Sleep(1000);
}
return result;
}
在任务开始后大概3秒钟的时候按下键盘,会得到如下的输出: 任务开始……
任务完成,完成时候的状态为: IsCanceled=False IsCompleted=True IsFaulted=False 任务的返回值为:3
你也许会奇怪,我们的任务是通过Cancel的方式处理,为什么完成的状态IsCanceled那一栏还是False。这是因为在工作任务中,我们 对于IsCancellationRequested进行了业务逻辑上的处理,并没有通过ThrowIfCancellationRequested方法 进行处理。
如果采用后者的方式,如下:
static void Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
Task t = new Task(() => AddCancleByThrow(cts.Token), cts.Token);
t.Start();
t.ContinueWith(TaskEndedByCatch); //等待按下任意一个键取消任务
Console.ReadKey();
cts.Cancel();
Console.ReadKey();
}
static void TaskEndedByCatch(Task task)
{
Console.WriteLine("任务完成,完成时候的状态为:");
Console.WriteLine("IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}", task.IsCanceled, task.IsCompleted, task.IsFaulted);
try
{
Console.WriteLine("任务的返回值为:{0}", task.Result);
}
catch (AggregateException e)
{
e.Handle((err) => err is OperationCanceledException);
}
}
static int AddCancleByThrow(CancellationToken ct)
{
Console.WriteLine("任务开始……");
int result = 0;
while (true)
{
ct.ThrowIfCancellationRequested();
result++;
Thread.Sleep(1000);
}
return result;
}
那么输出为: 任务开始…… 任务完成,完成时候的状态为: IsCanceled=True IsCompleted=True IsFaulted=False
在任务结束求值的方法TaskEndedByCatch中,如果任务是通过 ThrowIfCancellationRequested方法结束的, 对任务求结果值将会抛出异常OperationCanceledException,而不是得到抛出异常前的结果值。这意味着任务是通过异常的方式被取消 掉的,所以可以注意到上面代码的输出中,状态IsCancled为True。 再一次,我们注意到取消是通过异常的方式实现的,而表示任务中发生了异常的IsFaulted状态却还是等于False。
这是因为 ThrowIfCancellationRequested是协作式取消方式类型CancellationTokenSource的一个方法,CLR进行了特殊的处理。CLR知道这一行程序开发者有意为之的代码,
所以不把它看作是一个异常(它被理解为取消)。要得到IsFaulted等于True的状态, 我们可以修改While循环,模拟一个异常出来:
while (true)
{
//ct.ThrowIfCancellationRequested();
if (result == 5)
{
throw new Exception("error");
}
result++;
Thread.Sleep(1000);
}
模拟异常后的输出为: 任务开始…… 任务完成,完成时候的状态为: IsCanceled=False IsCompleted=True IsFaulted=True
三:任务工厂 Task还支持任务工厂的概念。
任务工厂支持多个任务之间共享相同的状态,如取消类型CancellationTokenSource就是可以被共享的。通过使用任务工厂,可以同时取消一组任务:
static void Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource(); //等待按下任意一个键取消任务
TaskFactory taskFactory = new TaskFactory();
Task[] tasks = new Task[]
{
taskFactory.StartNew(() => Add(cts.Token)),
taskFactory.StartNew(() => Add(cts.Token)),
taskFactory.StartNew(() => Add(cts.Token))
};
//CancellationToken.None指示TasksEnded不能被取消
taskFactory.ContinueWhenAll(tasks, TasksEnded, CancellationToken.None);
Console.ReadKey();
cts.Cancel();
Console.ReadKey();
}
static void TasksEnded(Task[] tasks)
{
Console.WriteLine("所有任务已完成!");
}
以上代码输出为: 任务开始…… 任务开始…… 任务开始…… 所有任务已完成(取消)! 本建议演示了Task(任务)和TaskFactory(任务工厂)的使用方法。Task甚至进一步优化了后台线程池的调度,加快了线程的处理速度。在FCL4.0时代,使用多线程,我们理应更多地使用Task
用Task代替ThreadPool
ThreadPool相对于Thread来说具有很多优势,但是ThreadPool在使用上却存在一定的不方便。比如:
ThreadPool不支持线程的取消、完成、失败通知等交互性操作。
ThreadPool不支持线程执行的先后次序。
以往,如果开发者要实现上述功能,需要完成很多额外的工作。现在,FCL中提供了一个功能更强大的概念:Task。Task在线程池的基础上进行了优化,并提供了更多的API。在FCL 4.0中,如果我们要编写多线程程序,Task显然已经优于传统的方式了。
因此,在本书接下来的建议中,如无特别必要,只要涉及多线程内容的,都将一并使用Task来完成。
使用Parallel简化同步状态下Task的使用
在命名空间System.Threading.Tasks中,有一个静态类Parallel简化了在同步状态下的Task的操作。Parallel主要提供3个有用的方法:For、ForEach、Invoke。
For方法主要用于处理针对数组元素的并行操作,如下所示:
static void Main(string[] args)
{
int[] nums = new int[] { 1, 2, 3, 4 };
Parallel.For(0, nums.Length, (i) =>
{
Console.WriteLine("针对数组索引{0}对应的那个元素{1}的一些工作代码……",i,
nums[i]);
});
Console.ReadKey();
}
输出为:
针对数组索引0对应的那个元素1的一些工作代码……
针对数组索引2对应的那个元素3的一些工作代码……
针对数组索引1对应的那个元素2的一些工作代码……
针对数组索引3对应的那个元素4的一些工作代码……
可以看到,工作代码并未按照数组的索引次序进行遍历。这是因为我们的遍历是并行的,不是顺序的。所以,这里也可以引出一个小建议:如果我们的输出必须是同步的或者说必须是顺序输出的,则不应使用Parallel的方式。
ForEach方法主要用于处理泛型集合元素的并行操作,如下所示:
static void Main(string[] args)
{
List nums = new List { 1, 2, 3, 4 };
Parallel.ForEach(nums, (item) =>
{
Console.WriteLine("针对集合元素{0}的一些工作代码……", item);
});
Console.ReadKey();
}
输出为:
针对集合元素1的一些工作代码……
针对集合元素4的一些工作代码……
针对集合元素3的一些工作代码……
针对集合元素2的一些工作代码……
使用For和ForEach方法,Parallel类型会自动为我们分配Task来完成针对元素的一些工作。当然我们也可以直接使用Task,但是上面的这种形式在语法上更为简洁。
Parallel的Invoke方法为我们简化了启动一组并行操作,它隐式启动的就是Task。该方法接受Params Action[ ]参数,如下所示:
static void Main(string[] args)
{
Parallel.Invoke(() =>
{
Console.WriteLine("任务1……");
},
() =>
{
Console.WriteLine("任务2……");
},
() =>
{
Console.WriteLine("任务3……");
});
Console.ReadKey();
}
输出为:
任务2……
任务3……
任务1……
同样,由于所有的任务都是并行的,所以它不保证先后次序。
Parallel简化但不等同于Task默认行为
上面说到了Parallel的使用方法,不知道大家是否注意到文中使用的字眼:在同步状态下简化了Task的使用。也就是说,在运行Parallel中的For、ForEach方法时,调用者线程(在示例中就是主线程)是被阻滞的。Parallel虽然将任务交给Task去处理,即交给CLR线程池去处理,不过调用者会一直等到线程池中的相关工作全部完成。表示并行的静态类Parallel甚至只提供了Invoke方法,而没有同时提供一个BeginInvoke方法,这也从一定程度上说明了这个问题。
在使用Task时,我们最常使用的是Start方法(Task也提供了RunSynchronously),它不会阻滞调用者线程。如下所示:
static void Main()
{
Task t = new Task(() =>
{
while (true)
{
}
});
t.Start();
Console.WriteLine("主线程即将结束");
Console.ReadKey();
}
输出为:
主线程即将结束
使用Parallel执行相近的功能,主线程被阻滞:
static void Main()
{
//在这里也可以使用Invoke方法
Parallel.For(0, 1, (i) =>
{
while (true)
{
}
});
Console.WriteLine("主线程即将结束");
Console.ReadKey();
}
如果执行这段代码,永远不会有输出。
并行编程,意味着运行时在后台将任务分配到尽量多的CPU上,虽然它在后台使用Task进行管理,但这并不意味着它等同于异步。
小心Parallel中的陷阱
Parallel的For和ForEach方法还支持一些相对复杂的应用。在这些应用中,它允许我们在每个任务启动时执行一些初始化操作,在每个任务结束后,又执行一些后续工作,同时,还允许我们监视任务的状态。但是,记住上面这句话“允许我们监视任务的状态”是错误的:应该把其中的“任务”改成“线程”。这,就是陷阱所在。
我们需要深刻理解这些具体的操作和应用,不然,极有可能陷入这个陷阱中去。下面体会这段代码的输出是什么,如下所示:
static void Main(string[] args)
{
int[] nums = new int[] { 1, 2, 3, 4 };
int total = 0;
Parallel.For(0, nums.Length, () =>
{
return 1;
}, (i, loopState, subtotal) =>
{
subtotal += nums[i];
return subtotal;
},
(x) => Interlocked.Add(ref total, x)
);
Console.WriteLine("total={0}", total);
Console.ReadKey();
}
这段代码有可能输出11,较少的情况下输出12,虽然理论上有可能输出13和14,但是我们应该很少有机会观察到。要明白为什么会有这样的输出,首先必须详细了解For方法的各个参数。上面这个For方法的声明如下:
public static ParallelLoopResult For(int fromInclusive,
int toExclusive, Func localInit, Func ParallelLoopState, TLocal, TLocal> body, Action localFinally);
前面两个参数相对容易理解,分别是起始索引和结束索引。
参数body也比较容易理解,即任务体本身。其中subtotal为单个任务的返回值。
localInit和localFinally就比较难理解了,并且陷阱也在这里。要理解这两个参数,必须先理解Parallel.For方法的运作模式。For方法采用并发的方式来启动循环体中的每个任务,这意味着,任务是交给线程池去管理的。在上面的代码中,循环次数共计4次,实际运行时调度启动的后台线程也就只有一个或两个。这就是并发的优势,也是线程池的优势,Parallel通过内部的调度算法,最大化地节约了线程的消耗。localInit的作用是如果Parallel为我们新起了一个线程,它就会执行一些初始化的任务在上面的例子中:
() =>
{
return 1;
}
它会将任务体中的subtotal这个值初始化为1。
localFinally的作用是,在每个线程结束的时候,它执行一些收尾工作:
(x) => Interlocked.Add(ref total, x)
这行代码所代表的收尾工作实际就是:
totaltotal = total + subtotal;
其中的x,其实代表的就是任务体中的返回值,具体在这个例子中就是subtotal在返回时的值。使用Interlocked是对total使用原子操作,以避免并发所带来的问题。
现在,我们应该很好理解为什么上面这段代码的输出会不确定了。Parallel一共启动了4个任务,但是我们不能确定Parallel到底为我们启动了多少个线程,那是运行时根据自己的调度算法决定的。如果所有的并发任务只用了一个线程,则输出为11;如果用了两个线程,那么根据程序的逻辑来看,输出就是12了。
在这段代码中,如果让localInit返回的值为0,也许你就永远不会注意到这个陷阱:
() =>
{
return 0;
}
现在,为了更清晰地体会这个陷阱,我们使用下面这段更好理解的代码:
static void Main(string[] args)
{
string[] stringArr = new string[] { "aa", "bb", "cc", "dd", "ee", "ff",
"gg", "hh" };
string result = string.Empty;
Parallel.For(0, stringArr.Length, () => "-", (i, loopState,
subResult) =>
{
return subResult += stringArr[i];
}, (threadEndString) =>
{
result += threadEndString;
Console.WriteLine("Inner:" + threadEndString);
});
Console.WriteLine(result);
Console.ReadKey();
}
这段代码的一个可能的输出为:
Inner:-aabbccddffgghh
Inner:-ee
-aabbccddffgghh-ee
使用PLINQ
第2章已经介绍过LINQ。LINQ最基本的功能就是对集合进行遍历查询,并在此基础上对元素进行操作。仔细推敲会发现,并行编程简直就是专门为这一类应用准备的。因此,微软专门为LINQ拓展了一个类ParallelEnumerable(该类型也在命名空间System.Linq中),它所提供的扩展方法会让LINQ支持并行计算,这就是所谓的PLINQ。
传统的LINQ计算是单线程的,PLINQ则是并发的、多线程的,我们通过下面这个示例就可以看出这个区别:
static void Main(string[] args)
{
List intList = new List() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var query = from p in intList select p;
Console.WriteLine("以下是LINQ顺序输出:");
foreach (int item in query)
{
Console.WriteLine(item.ToString());
}
Console.WriteLine("以下是PLINQ并行输出:");
var queryParallel = from p in intList.AsParallel() select p;
foreach (int item in queryParallel)
{
Console.WriteLine(item.ToString());
}
}
LINQ的输出会按照intList中的索引顺序打印出来。而PLINQ的输出是杂乱无章的。
并行输出还有另外一种方式可以处理,那就是对queryParallel求ForAll:
queryParallel.ForAll((item) =>
{
Console.WriteLine(item.ToString());
});
但是这种方法会带来一个问题,如果要将并行输出后的结果进行排序,ForAll会忽略掉查询的AsOrdered请求。如下所示:
var queryParallel = from p in intList.AsParallel().AsOrdered() select p;
queryParallel.ForAll((item) =>
{
Console.WriteLine(item.ToString());
});
AsOrdered方法可以对并行计算后的队列进行重新组合,以便保持顺序。可是在ForAll方法中,它所完成的输出仍是无序的。如果要保持AsOrdered方法的需求,我们应当始终使用第一种并行方式,即:
var queryParallel = from p in intList.AsParallel().AsOrdered() select p;
foreach (int item in queryParallel)
{
Console.WriteLine(item.ToString());
}
在并行查询后再进行排序,会牺牲掉一定的性能。一些扩展方法默认会对元素进行排序,这些方法包括:OrderBy、OrderByDescending、ThenBy和ThenByDescending。在实际的使用中,一定要注意到各种方式之间的差别,以便程序按照我们的设想运行。
还有一些其他的查询方法,比如Take。如果我们这样编码:
foreach (int item in queryParallel.Take(5))
{
Console.WriteLine(item.ToString());
}
在顺序查询中,会返回前5个元素。但是在PLINQ中,会选出5个无序的元素。
建议在对集合中的元素项进行操作的时候使用PLINQ代替LINQ。但是要记住,不是所有并行查询的速度都会比顺序查询快,在对集合执行某些方法时,顺序查询的速度会更快一点,如方法ElementAt等。在开发中,我们应该仔细辨别这方面的需求,以便找到最佳的解决方案。
await是Task的异步等待而已,并不是“异步操作”
微软官方的MSDN上说async和await是“异步”,但是不少人(包括笔者自己)都有一些误区需要澄清:为什么await语句之后没有执行?不是异步吗?
先举一个示例代码如下:
public partial class Form1 : Form
{
public async Task Processing()
{
await Task.Delay(5000);
label1.Text = "Succuessful";
}
public Form1()
{
InitializeComponent();
}
private async void button1_Click(object sender, EventArgs e)
{
await Processing();
MessageBox.Show("Button's event completed");
}
}
很多人(包括笔者)一开始会觉得异步好像类似多线程一样,到await的时候会在后台先开启一个线程执行任务,随后主线程(这里是UI线程)将自动执行后面的部分(即弹出“Button's event completed”的消息框)。
其实这个理解是错误的。async和await的本质其实是“yield return”和“LINQ”的“迭代式”等待。我们应该清楚一点:那就是你写了LINQ语句:
var results = from ……
select ……;
foreach(var r in results)
{
……
}
当你下断点你会发觉results并不会立即执行,直到使用到results的地方(例子中也就是foreach这里)才会被执行(此时黄色跟踪调试的光棒又会折回到var results……这里,然后等到results执行完毕之后才真正进入foreach进行执行)。
所以,async/await和LINQ的这种“迭代式”的“异步操作”是异曲同工的。只不过async/await本质是返回一个Task而已,而Task又是异步的(因为Task本质就是一个线程),所以真正执行到(使用到async方法的时候)带有await的方法的时候,后台才会真正开启一个线程去执行任务。此时主线程会等待这个Task线程直到其执行完毕(IsComplete属性为True为止)。所以界面是不会卡顿的。
所以,await是Task的异步等待而已,并不是我们所谓的“异步操作”;拿它和LINQ作对比,你会发现LINQ执行顺序和它一致,只不过LINQ没有异步等待(当然没有!又没有开启线程啥的……)。
我们进一步可以这样对比:
LINQ:变量 = LINQ语句(表达式)
等到使用LINQ变量的时候才折返到LINQ语句处真正执行LINQ语句。
异步等待:变量 = 异步方法
等到使用await+异步方法的时候才会折返到该异步方法处,开启线程真正执行异步方法,主线程被挂起(但不会造成界面死掉),直至子线程Task任务完全执行完毕为止。
在LINQ中,你如果需要立即执行,可以使用扩展方法:
var results = (from ……
select ……).ToList();
因为立即使用到了这个LINQ语句,所以会被立即执行。
同样地,异步等待也可以变成类似Wait一样的同步等待:
private async void button1_Click(object sender, EventArgs e)
{
Processing().GetAwaiter().GetResult();
MessageBox.Show("Button's event completed");
}
因为Processing本来就返回Task,当然也可以使用Wait进行同步等待。
await使用中的阻塞和并发
在.NET4.5中,我们可以配合使用async和await两个关键字,来以写同步代码的方式,实现异步的操作。
好处我目前看来有两点:
1.不会阻塞UI线程。一旦UI线程不能及时响应,会极大的影响用户体验,这点在手机和平板的APP上尤为重要。
2.代码简洁。
相对基于event的异步方式,在多次回调的情况下(比如需要多次调web service,且后续调用基于前次调用的结果)特别明显。可以将多个+=Completed方法合并到一起。
相对于Begin/End的异步方式,避免了N重且不能对齐的大括号。
在同一个方法里存在多个await的情况下,如后续Async方法无需等待之前的Aysnc方法返回的结果,会隐式的以并行方式来运行后面的Async方法。
值得注意的是错误的写法会导致非预期的阻塞,下文会以简单的例子来讨论在使用await的情况下,怎样实现多个Task的并发执行。
我们这里先看几个方法定义:
static async Task Delay3000Async()
{
await Task.Delay(3000);
Console.WriteLine(3000);
Console.WriteLine(DateTime.Now);
}
static async Task Delay2000Async()
{
await Task.Delay(2000);
Console.WriteLine(2000);
Console.WriteLine(DateTime.Now);
}
static async Task Delay1000Async()
{
await Task.Delay(1000);
Console.WriteLine(1000);
Console.WriteLine(DateTime.Now);
}
作用很简单,仅仅是起到延迟的作用。我们再来看如下写法的调用
Console.WriteLine(DateTime.Now);
new Action(async () =>
{
await Delay3000Async();
await Delay2000Async();
await Delay1000Async();
})();
结果如图,略,可以看出3个await是线性执行,第一个await会返回并阻止接下来的await后面的方法。这应该不是我们想要的效果,毕竟后面的方法并不依赖第一个方法的执行。
我们换一种写法,再运行一次程序:
var task3 = Delay3000Async();
var task2 = Delay2000Async();
var task1 = Delay1000Async();
new Action(async () =>
{
await task3;
await task2;
await task1;
})();
可以看到3个await后面的方法是并行执行的。MSDN的解释如下:
In an async method, tasks are started when they’re created. The Await (Visual Basic) or await (C#) operator is applied to the task at the point in the method where processing can’t continue until the task finishes.
However, you can separate creating the task from awaiting the task if your program has other work to accomplish that doesn’t depend on the completion of the task.
Between starting a task and awaiting it, you can start other tasks. The additional tasks implicitly run in parallel, but no additional threads are created.
到这里并没有结束 ,后面还有一些奇怪的事情:
var tasks = new List
{
Delay3000Async(),
Delay2000Async(),
Delay1000Async()
};
tasks.ForEach(async _ => await _);
这个结果和上面是一样的,可以并行执行。这并不奇怪,我们仅仅是把Task放到一个List里,按照MSDN的说法,Task在被我们放进List时就被创建,且并发执行了。
那么我们再来一个List,这回放进去的不是Task,而是Func:
var funcList = new List>()
{
Delay3000Async,
Delay2000Async,
Delay1000Async
};
funcList.ForEach(async _ => await _());
仍然可以并发执行,看上去似乎没什么问题,但是作为Func来存储到List里,应该是没有被创建出来才对。为什么会能够并发呢?
我们再来看最后一组写法:
Func func3 = Delay3000Async;
Func func2 = Delay2000Async;
Func func1 = Delay1000Async;
new Action(async () =>
{
await func3();
await func2();
await func1();
}
)();
意料之中的,以上的写法并不能够做到并发执行。而是需要按顺序执行func3,func2和func1。这很好解释,因为: a task is awaited as soon as it’s created。我们在创建Task之后立即就要求阻塞并等待完成才进行下一步。
一些概念和坑
--begin--
通过Task.Run() 将同步方法包装成异步任务是否真的有益处?
这取决于你的目标,你为什么要异步调用方法。如果你的目标只是想将当前任务切换到另一个线程执行,比如,保证UI线程的响应能力,那么肯定有益。如果你的目标是为了提高可扩展性,那么使用Task.Run() 包装成异步调用将没有任何实际意义。更多信息,请看《我是否应该公开同步方法对应的异步方法API?》。通过Task.Run() 你可以很轻松的实现从UI线程分担工作到另一个工作线程,且可协调后台线程一旦完成工作就返回到UI线程。
“async”关键字
将关键字”async”应用到方法上的作用是什么?
当你用关键字”async”标记一个方法时,即告诉了编译器两件事:
1) 你告诉编译器,想在方法内部使用”await”关键字(只有标记了”async”关键字的方法或lambda表达式才能使用”await”关键字)。这样做后,编译器会将方法转化为包含状态机的方法(类似的还有yield的工作原理,请看 《C#稳固基础:传统遍历与迭代器》 ),编译后的方法可以在await处挂起并且在await标记的任务完成后异步唤醒。
2) 你告诉编译器,方法的结果或任何可能发生的异常都将作为返回类型返回。如果方法返回Task或Task,这意味着任何结果值或任何在方法内部未处理的异常都将存储在返回的Task中。如果方法返回void,这意味着任何异常会被传播到调用者上下文。
被”async”关键字标记的方法的调用都会强制转变为异步方式吗?
不会,当你调用一个标记了”async”关键字的方法,它会在当前线程以同步的方式开始运行。所以,如果你有一个同步方法,它返回void并且你做的所有改变只是将其标记的”async”,这个方法调用依然是同步的。返回值为Task或Task也一样。
方法用”async”关键字标记不会影响方法是同步还是异步运行并完成,而是,它使方法可被分割成多个片段,其中一些片段可能异步运行,这样这个方法可能异步完成。这些片段界限就出现在方法内部显示使用”await”关键字的位置处。所以,如果在标记了”async”的方法中没有显示使用”await”,那么该方法只有一个片段,并且将以同步方式运行并完成。
“async”关键字会导致调用方法被排队到ThreadPool吗?会创建一个新的线程吗?
全都不会,”async”关键字指示编译器在方法内部可能会使用”await”关键字,这样该方法就可以在await处挂起并且在await标记的任务完成后异步唤醒。这也是为什么编译器在编译”async” 标记的方法时,方法内部没有使用”await”会出现警告的原因。
”async”关键字能标记任何方法吗?
不能,只有返回类型为void、Task或Task的方法才能用”async”标记。并且,并不是所有返回类型满足上面条件的方法都能用”async”标记。如下,我们不允许使用”async”标记方法:
1) 在程序的入口方法(eg:Main()),不允许。当你正在await的任务还未完成,但执行已经返回给方法的调用者了。Eg:Main(),这将退出Main(),直接导致退出程序。
2) 在方法包含如下特性时,不允许。
l [MethodImpl(MethodImplOptions.Synchronized)]
为什么这是不允许的,详细请看《What’s New for Parallelism in .NET 4.5 Beta》。此特性将方法标记为同步类似于使用lock/SyncLock同步基元包裹整个方法体。
l [SecurityCritical]和[SecuritySafeCritical] (Critical:关键)
编译器在编译一个”async”标记的方法,原方法体实际上最终被编译到新生成的MoveNext()方法中,但是其标记的特性依然存在。这意味着特性如[SecurityCritical]不会正常工作。
3) 在包含ref或out参数的方法中,不允许。调用者期望方法同步调用完成时能确保设置参数值,但是标记为”async”的方法可能不能保证立刻设置参数值直到异步调用完成。
4) Lambda被用作表达式树时,不允许。异步lambda表达式不能被转换为表达式树。
是否有任何约定,这时应该使用”async”标记方法?
有,基于任务的异步编程模型(TAP)是完全专注于怎样实现异步方法,这个方法返回Task或Task。这包括(但不限于)使用”async”和”await”关键字实现的方法。想要深入TAP,请看《基于任务的异步编程模型》文档。
“async”标记的方法创建的Tasks是否需要调用”Start()”?
不需要,TAP方法返回的Tasks是已经正在操作的任务。你不仅不需要调用”Start()”,而且如果你尝试也会失败。更多细节,请看《FAQ on Task.Start》 。
“async”标记的方法创建的Tasks是否需要调用”Dispose()”?
不需要,一般来说,你不需要 Disposer() 任何任务。请看《我需要释放任务吗?》。
“async”是如何关联到当前SynchronizationContext?
对于”async” 标记的方法,如果返回Task或Task,则没有方法级的SynchronizationContext交互;对于”async” 标记的方法,如果返回void,则有一个隐藏的SynchronizationContext交互。
当一个”async void”方法被调用,方法调用的开端将捕获当前SynchronizationContext(“捕获”在这表示访问它并且将其存储)。如果这里有一个非空的SynchronizationContext,将会影响两件事:(前提:”async void”)
1) 在方法调用的开始将导致调用捕获SynchronizationContext.OperationStarted()方法,并且在完成方法的执行时(无论是同步还是异步)将导致调用捕获SynchronizationContext.OprationCompleted()方法。这给上下文引用计数未完成异步操作提供时机点。如果TAP方法返回Task或Task,调用者可通过返回的Task做到同样的跟踪。
2) 如果这个方法是因为未处理的异常导致方法完成,那么这个异常将会提交给捕获的SynchronizationContext。这给上下文一个处理错误的时机点。如果TAP方法返回Task或Task,调用者可通过返回的Task得到异常信息。
当调用”async void”方法时如果没有SynchronizationContext,没有上下文被捕获,然后也不会调用OperaionStarted/OperationCompleted方法。在这种情况下,如果存在一个未处理过的异常在ThreadPool上传播,那么这会采取线程池线程默认行为,即导致进程被终止。
“await”关键字
“await”关键字做了什么
“await”关键字告诉编译器在”async”标记的方法中插入一个可能的挂起/唤醒点。
逻辑上,这意味着当你写”await someObject;”时,编译器将生成代码来检查someObject代表的操作是否已经完成。如果已经完成,则从await标记的唤醒点处继续开始同步执行;如果没有完成,将为等待的someObject生成一个continue委托,当someObject代表的操作完成的时候调用continue委托。这个continue委托将控制权重新返回到”async”方法对应的await唤醒点处。
返回到await唤醒点处后,不管等待的someObject是否已经经完成,任何结果都可从Task中提取,或者如果someObject操作失败,发生的任何异常随Task一起返回或返回给SynchronizationContext。
在代码中,意味着当你写:
Await someObject; 编译器会将源代码转化为类似如下形式:
private class FooAsyncStateMachine : IAsyncStateMachine
{
// Member fields for preserving “locals” and other necessary state
int $state;
TaskAwaiter $awaiter;
…
public void MoveNext()
{
// Jump table to get back to the right statement upon resumption
switch (this.$state)
{
…
case 2: goto Label2;
…
}
…
// Expansion of “await someObject;”
this.$awaiter = someObject.GetAwaiter();
if (!this.$awaiter.IsCompleted)
{
this.$state = 2;
this.$awaiter.OnCompleted(MoveNext);
return;
Label2:
}
this.$awaiter.GetResult();
…
}
}
什么是”awaitables”?什么是”awaiters”?
虽然Task和Task是两个非常普遍的等待类型(“awaitable”),但这并不表示只有这两个的等待类型。
“awaitable”可以是任何类型,它必须公开一个GetAwaiter() 方法并且返回有效的”awaiter”。这个GetAwaiter() 可能是一个实例方法(eg:Task或Task的实例方法),或者可能是一个扩展方法。
“awaiter”是”awaitable”对象的GetAwaiter()方法返回的符合特定的模式的类型。”awaiter”必须实现System.Runtime.CompilerServices.INotifyCompletion接口(,并可选的实现System.Runtime.CompilerServices.ICriticalNotifyCompletion接口)。除了提供一个INotifyCompletion接口的OnCompleted方法实现(,可选提供ICriticalNotifyCompletion接口的UnsafeCompleted方法实现),还必须提供一个名为IsCompleted的Boolean属性以及一个无参的GetResult()方法。GetResult()返回void,如果”awaitable”代表一个void返回操作,或者它返回一个TResult,如果”awaitable”代表一个TResult返回操作。
几种方法来实现自定义的”awaitable” 谈论,请看《await anything》。也能针对特殊的情景实现自定义”awaitable”,请看《Advanced APM Consumption in Async Methods》和《Awaiting Socket Operations》。
哪些地方不能使用”await”?
1) 在未标记”async”的方法或lambda表达式中,不能使用”await”。”async”关键字告诉编译器其标记的方法内部可以使用”await”。(更详细,请看《Asynchrony in C# 5 Part Six: Whither async?》)
2) 在属性的getter或setter访问器中,不能使用”await”。属性的意义是快速的返回给调用者,因此不期望使用异步,异步是专门为潜在的长时间运作的操作。如果你必须在你的属性中使用异步,你可以通过实现异步方法然后在你的属性中调用。
3) 在lock/SyncLock块中,不能使用”await”。关于谈论为什么不允许,以及SemaphoreSlim.WaitAsync(哪一个能用于此情况的等待),请看《What’s New for Parallelism in .NET 4.5 Beta》。你还可以阅读如下文章,关于如何构建各种自定义异步同步基元:
a) Building Async Coordination Primitives, Part 1: AsyncManualResetEvent
b) Building Async Coordination Primitives, Part 2: AsyncAutoResetEvent
c) Building Async Coordination Primitives, Part 3: AsyncCountdownEvent
d) Building Async Coordination Primitives, Part 4: AsyncBarrier
e) Building Async Coordination Primitives, Part 5: AsyncSemaphore
f) Building Async Coordination Primitives, Part 6: AsyncLock
g) Building Async Coordination Primitives, Part 7: AsyncReaderWriterLock
4) 在unsafe区域中,不能使用”await”。注意,你能在标记”async”的方法内部使用”unsafe”关键字,但是你不能在unsafe区域中使用”await”。
5) 在catch块和finally块中,不能使用”await”。你能在try块中使用”await”,不管它是否有相关的catch块和finally块,但是你不能在catch块或finally块中使用”await”。这样做会破坏CLR的异常处理。
6) LINQ中大部分查询语法中,不能使用”await”。”await”可能只用于查询表达式中的第一个集合表达式的”from”子句或在集合表达式中的”join”子句。
“await task;”和”task.Wait”效果一样吗?
不。
“task.Wait()”是一个同步,可能阻塞的调用。它不会立刻返回到Wait()的调用者,直到这个任务进入最终状态,这意味着已进入RanToCompletion,Faulted,或Canceled完成状态。相比之下,”await task;”告诉编译器在”async”标记的方法内部插入一个隐藏的挂起/唤醒点,这样,如果等待的task没有完成,异步方法也会立马返回给调用者,当等待的任务完成时唤醒它从隐藏点处继续执行。当”await task;”会导致比较多应用程序无响应或死锁的情况下使用“task.Wait()”。更多信息请看《Await, and UI, and deadlocks! Oh my!》。
当你使用”async”和”await”时,还有其他一些潜在缺陷。Eg:
1) 避免传递lambda表达式的潜在缺陷
2) 保证”async”方法不要被释放
3) 不要忘记完成你的任务
4) 使用”await”依然可能存在死锁?
“task.Result”与”task.GetAwaiter().GetResult()”之间存在功能区别吗?
存在。但仅仅在任务以非成功状态完成的情况下。如果task是以RanToCompletion状态完成,那么这两个语句是等价的。然而,如果task是以Faulted或Canceled状态完成,task.Result将传播一个或多个异常封装而成的AggregateException对象;而”task.GetAwaiter().GetResult()”将直接传播异常(如果有多个任务,它只会传播其中一个)。关于为什么会存在这个差异,请看《.NET4.5中任务的异常处理》。
“await”是如何关联到当前SynchronizationContext?
这完全取决于被等待的类型。对于给定的”awaitable”,编译器生成的代码最终会调用”awaiter”的OnCompleted()方法,并且传递将执行的continue委托。编译器生成的代码对SynchronizationContext一无所知,仅仅依赖当等待的操作完成时调用OnCompleted()方法时所提供的委托。这就是OnCompleted()方法,它负责确保委托在”正确的地方”被调用,”正确的地方”完全由”awaiter”决定。
正在等待的任务(由Task和Task的GetAwaiter方法分别返回的TaskAwaiter和TaskAwaiter类型)的默认行为是在挂起前捕获当前的SynchronizationContext,然后等待task的完成,如果能捕获到当前的SynchronzationContext,调用continue委托将控制权返回到SynchronizationContext中。所以,例如,如果你在应用程序的UI线程上执行”await task;”,如果当前SynchronizationContext非空则将调用OnCompleted(),并且在任务完成时,将使用UI的SynchronizationContext传播continue委托返回到UI线程。
当你等待一个任务,如果没有当前SynchronizationContext,那么系统会检查当前的TaskScheduler,如果有,当task完成时将使用TaskScheduler调度continue委托。
如果SynchronizationContext和TaskScheduler都没有,无法迫使continue委托返回到原来的上下文,或者你使用”await task.ConfigureAwait(false)代替”await task;”,然后continue委托不会迫使返回到原来上下文并且将允许在系统认为合适的地方继续运行。这通常意味着要么以同步方式运行continue,委托无论等待的task在哪完成;要么使用ThreadPool中的线程运行continue委托。
在控制台程序中能使用”await”吗?
当然能。但你不能在Main()方法中使用”await”,因为入口点不能被标记为”async”。相反,你能在控制台应用程序的其他方法中使用”await”。如果你在Main()中调用这些方法,你可以同步等待(而不是异步等待)他们的完成。Eg:
你还可以使用自定义的SynchronizationContext或TaskScheduler来实现相似的功能,更多信息请看:
1) Await, SynchronizationContext, and Console Apps: Part 1
2) Await, SynchronizationContext, and Console Apps: Part 2
3) Await, SynchronizationContext, and Console Apps: Part 3
“await”能和异步编程模型模式(APM)或基于事件的异步编程模式(EAP)一起使用吗?
当然能,你可以为你的异步操作实现一个自定义的”awaitable”,或者将你现有的异步操作转化为现有的”awaitable”,像task或task。示例如下:
1) Tasks and the APM Pattern
2) Tasks and the Event-based Asynchronous Pattern
3) Advanced APM Consumption in Async Methods
4) Implementing a SynchronizationContext.SendAsync method
5) Awaiting Socket Operations
6) await anything
7) The Nature of TaskCompletionSource
编译器对async/await生成的代码是否能高效异步执行?
大多数情况下,是的。因为大量的生成代码已经被编译器所优化并且.NET Framework也为生成代码建立依赖关系。要了解更多信息,包括使用async/await的最小化开销的最佳实践等。请看
1) .NET4.5对TPL的性能提升
2) 2012年MVP峰会上的“The Zen of Async”
3) 《了解 Async 和 Await 的成本》
:Task的优势 ThreadPool相比Thread来说具备了很多优势,但是ThreadPool却又存在一些使用上的不方便。比如: 1: ThreadPool不支持线程的取消、完成、失败通知等交互性操作; 2: ThreadPool不支持线程执行的先后次序;
--end--