C# 5.0 搭载于.NET 4.5和VS2012之上。

  同步操作既简单又方便,我们平时都用它。但是对于某些情况,使用同步代码会严重影响程序的可响应性,通常来说就是影响程序性能。这些情况下,我们通常是采用异步编程来完成功能,这在前面也多次提及了。异步编程的核心原理也就是使用多线程/线程池和委托来完成任务的异步执行和返回,只不过在每个新的C#版本中,微软都替我们完成了更多的事,使得程序模板越来越傻瓜化了。

  .NET Framework 提供以下两种执行 I/O 绑定和计算绑定异步操作的标准模式:
1. 异步编程模型 (APM,Asynchronous Programming Model)

  在该模型中异步操作由一对 Begin/End 方法(如 FileStream.BeginRead 和 Stream.EndRead)表示。
  异步编程模型是一种模式,该模式使用更少的线程去做更多的事。.NET Framework很多类实现了该模式,这些类都定义了BeginXXX和EndXXX类似的方法,比如FileStream类的BeginRead和EndRead方法。同时我们也可以自定义类来实现该模式(也就是在自定义的类中实现返回类型为IAsyncResult接口的BeginXXX方法和EndXXX方法);另外委托类型也定义了BeginInvoke和EndInvoke方法,使得委托可以异步执行。这些异步操作的背后都是线程池在支撑着,这是微软异步编程的基础架构,也是比较老的模式,不过从中我们可以清楚的了解异步操作的原理。

  所有BeginXXX方法返回的都是实现了IAsyncResult接口的一个对象,并不是对应的同步方法所要得到的结果的。此时我们需要调用对应的EndXXX方法来结束异步操作,并向该方法传递IAsyncResult对象,EndXxx方法的返回类型就是和同步方法一样的。例如,FileStream的EndRead方法返回一个Int32来代表从文件流中实际读取的字节数。

  对于访问异步操作的结果,APM提供了四种方式供开发人员选择:

- 在调用BeginXxx方法的线程上调用EndXXX方法来得到异步操作的结果,但是这种方式会阻塞调用线程,直到操作完成之后调用线程才继续运行
- 查询IAsyncResult的AsyncWaitHandle属性,从而得到WaitHandle,然后再调用它的WaitOne方法来使一个线程阻塞并等待操作完成再调用EndXxx方法来获得操作的结果。
- 循环查询IAsyncResult的IsComplete属性,操作完成后再调用EndXxx方法来获得操作返回的结果。
- 使用 AsyncCallback委托来指定操作完成时要调用的方法,在操作完成后调用的方法中调用EndXxx操作来获得异步操作的结果。
  在上面的4种方式中,第4种方式是APM的首选方式,因为此时不会阻塞执行BeginXxx方法的线程,然而其他三种都会阻塞调用线程,相当于效果和使用同步方法是一样,在实际异步编程中都是使用委托的方式。

看一个简答的例子:

using System;
using System.Net;
using System.Threading; class Program
{
static DateTime start;
static void Main(string[] args)
{
// 用百度分别检索0,1,2,3,4,共检索5次
start = DateTime.Now;
string strReq = "http://www.baidu.com/s?wd={0}";
for (int i = ; i < ; i++)
{
var req = WebRequest.Create(string.Format(strReq, i));
// 注意这里的BeginGetResponse就是异步方法
var res = req.BeginGetResponse(ProcessWebResponse, req);
} Thread.Sleep();
} private static void ProcessWebResponse(IAsyncResult result)
{
var req = (WebRequest)result.AsyncState;
string strReq = req.RequestUri.AbsoluteUri;
using (var res = req.EndGetResponse(result))
{
Console.Write("检索 {0} 的结果已经返回!\t", strReq.Substring(strReq.Length - ));
Console.WriteLine("耗用时间:{0}毫秒", TimeSpan.FromTicks(DateTime.Now.Ticks - start.Ticks).TotalMilliseconds);
}
}
}

结构相当简单,使用了回调函数获取结果,就不多说了。

2. 基于事件的异步模式 (EAP,Event based Asynchronous programming Model)

  在该模式中异步操作由名为“XXXAsync”和“XXXCompleted”的方法/事件表示,例如WebClient.DownloadStringAsync 和 WebClient.DownloadStringCompleted,还有像常用的BackgroundWorker.RunWorkerAsync和BackgroundWorker.RunWorkerCompleted方法。
  EAP 是在 .NET Framework 2.0 版中引入的。使用陈旧的BeginXXX和EndXXX方法无疑是不够优雅的,并且程序员需要写更多的代码,特别是在UI程序中使用不太方便。UI的各种操作基本都是基于事件的,而且通常来说UI线程和子线程之间还需要互相交流,比如说显示进度,警告,相关的消息等等,直接在子线程中访问UI线程上的空间是需要写一些同步代码的。这些操作使用APM处理起来都比较麻烦,而EAP则很好的解决了这些问题,EAP里面最出色的代表就应该是BackgroundWorker类了。
  看一个网上一位仁兄写的下载的小例子:

private void btnDownload_Click(object sender, EventArgs e)
{
if (bgWorkerFileDownload.IsBusy != true)
{
// 开始异步执行DoWork中指定的任务
bgWorkerFileDownload.RunWorkerAsync(); // 创建RequestState对象
requestState = new RequestState(downloadPath);
requestState.filestream.Seek(DownloadSize, SeekOrigin.Begin);
this.btnDownload.Enabled = false;
this.btnPause.Enabled = true;
}
else
{
MessageBox.Show("正在执行操作,请稍后");
}
} private void btnPause_Click(object sender, EventArgs e)
{
// 暂停的标准处理方式:先判断标识,然后异步申请暂停
if (bgWorkerFileDownload.IsBusy && bgWorkerFileDownload.WorkerSupportsCancellation == true)
{
bgWorkerFileDownload.CancelAsync();
}
} // 指定Worker的工作任务,当RunWorkerAsync方法被调用时开始工作
// 这是在子线程中执行的,不允许访问UI上的元素
private void bgWorkerFileDownload_DoWork(object sender, DoWorkEventArgs e)
{
// 获取事件源
BackgroundWorker bgworker = sender as BackgroundWorker; // 开始下载
HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(txbUrl.Text.Trim()); // 断点续传的功能
if (DownloadSize != )
{
myHttpWebRequest.AddRange(DownloadSize);
} requestState.request = myHttpWebRequest;
requestState.response = (HttpWebResponse)myHttpWebRequest.GetResponse();
requestState.streamResponse = requestState.response.GetResponseStream();
int readSize = ;
// 前面讲过的异步取消中子线程的工作:循环并判断标识
while (true)
{
if (bgworker.CancellationPending == true)
{
e.Cancel = true;
break;
} readSize = requestState.streamResponse.Read(requestState.BufferRead, , requestState.BufferRead.Length);
if (readSize > )
{
DownloadSize += readSize;
int percentComplete = (int)((float)DownloadSize / (float)totalSize * );
requestState.filestream.Write(requestState.BufferRead, , readSize); // 报告进度,引发ProgressChanged事件的发生
bgworker.ReportProgress(percentComplete);
}
else
{
break;
}
}
} // 当Worker执行ReportProgress时回调此函数。此函数在UI线程中执行更新操作进度的任务
// 因为是在在主线程中工作的,可以与UI上的元素交互
private void bgWorkerFileDownload_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
this.progressBar1.Value = e.ProgressPercentage;
} // 当Worker结束时触发的回调函数:也许是成功完成的,或是取消了,或者是抛异常了。
// 这个方法是在UI线程中执行,所以可以与UI上的元素交互
private void bgWorkerFileDownload_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
{
MessageBox.Show(e.Error.Message);
requestState.response.Close();
}
else if (e.Cancelled)
{
MessageBox.Show(String.Format("下载暂停,下载的文件地址为:{0}\n 已经下载的字节数为: {1}字节", downloadPath, DownloadSize));
requestState.response.Close();
requestState.filestream.Close(); this.btnDownload.Enabled = true;
this.btnPause.Enabled = false;
}
else
{
MessageBox.Show(String.Format("下载已完成,下载的文件地址为:{0},文件的总字节数为: {1}字节", downloadPath, totalSize)); this.btnDownload.Enabled = false;
this.btnPause.Enabled = false;
requestState.response.Close();
requestState.filestream.Close();
}
} private void GetTotalSize()
{
HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(txbUrl.Text.Trim());
HttpWebResponse response = (HttpWebResponse)myHttpWebRequest.GetResponse();
totalSize = response.ContentLength;
response.Close();
} // 存储申请的状态
public class RequestState
{
public int BufferSize = ; public byte[] BufferRead;
public HttpWebRequest request;
public HttpWebResponse response;
public Stream streamResponse; public FileStream filestream;
public RequestState(string downloadPath)
{
BufferRead = new byte[BufferSize];
request = null;
streamResponse = null;
filestream = new FileStream(downloadPath, FileMode.OpenOrCreate);
}
}

  上面的例子就是实现了一个可以取消的带断点续传功能的下载器,这是个Winform程序,控件也很简单:一个Label,一个Textbox,两个Button,一个ProgressBar;把这些控件和上面的事件对应绑定即可。

  在.NET 4.0 (C# 4.0)中,并行库(TPL)的加入使得异步编程更加方便快捷,在.NET 4.5 (C# 5.0)中,异步编程将更加方便。

  这里我们先回顾一下C# 4.0中的TPL的用法,看一个简单的小例子:这个例子中只有一个Button和一个Label,点击Button会调用一个函数计算一个结果,这个结果最后会显示到Label上,很简单,我们只看核心的代码:

private void button1_Click(object sender, EventArgs e)
{
this.button1.Enabled = false;
var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); //get UI thread context
var someTask = Task<int>.Factory.StartNew(() => slowFunc(, )); //create and start the Task
someTask.ContinueWith(x =>
{
this.label1.Text = "Result: " + someTask.Result.ToString();
this.button1.Enabled = true;
}, uiScheduler
);
} private int slowFunc(int a, int b)
{
System.Threading.Thread.Sleep();
return a + b;
}

  上面的slowFunc就是模拟了一个需要大量时间去运行的任务,为了不阻塞UI线程,只能使用Task去异步运行,为了在把结果显示到Label上,代码中我们使用了TaskScheduler.FromCurrentSynchronizationContext()方法同步线程上下文,使得在ContinueWith方法中可以使用UI线程上的控件,这是TPL编程中的一个常用技巧。
  说不上太麻烦,但是感觉上总之不舒服,完全没有同步代码写起来那么自然,简单。从我个人的理解来说,C# 5.0中的async和await正是提高了这方面的用户体验。
  C# 5.0中的async和await特性并没有在IL层面增加了新的成员,所以也可以说是一种语法糖。下面先看看再C# 5.0中如何解决这个问题:

private async void button1_Click(object sender, EventArgs e)
{
this.button1.Enabled = false;
var someTask = Task<int>.Factory.StartNew(() => slowFunc(, ));
await someTask;
this.label1.Text = "Result: " + someTask.Result.ToString();
this.button1.Enabled = true;
}

  注意这段代码中的async和await的用法。除了这个事件处理函数,其他的都没有变化。是不是很神奇,完全和同步代码没什么太大的区别,很是简单优雅,完全是同步方式的异步编程
  下面我们就详细的讨论一下async和await这两个关键字。

async和await
  通过使用async修饰符,可将方法、lambda表达式或匿名方法指定为异步。 使用了这个修饰符的方法或表达式,则其称为异步方法,如上面的button1_Click方法就是一个异步方法。
  异步方法提供了一种简便方式来完成可能需要长时间运行的工作,而不必阻塞调用方的线程。 异步方法的调用方(这里就是button1_Click的调用者)可以继续工作,而不必等待异步方法button1_Click完成。 完成这个特性需要使用 await 关键字,以便立即返回,从而允许button1_Click的调用方继续工作或返回到线程的同步上下文(或消息泵)。
  从上面的描述中得到,异步方法更准确的定义应该是:使用async修饰符定义的,且通常包含一个或多个await表达式的方法称为异步方法
  如果async关键字修饰的方法不包含await表达式或语句,则该方法仍将同步执行。 对于这种情况,编译器将会给出警告,因为该情况通常表示程序可能存在错误。 也就是说,单单使用async修饰符的方法还是在同步执行的,只有配合await关键字后方法的部分才开始异步执行。

  await表达式不阻塞主线程。 相反,它告诉编译器去重写异步方法来完成下面几件事:
1. 启动子线程(通常是线程池中的线程)完成await表达式中指定的任务,这是异步执行的真正含义。
2. 将await表达式后面未执行的语句注册为await表达式中执行的任务的后续任务,然后挂起这个异步方法,直接返回到异步方法的调用方。
3. 当await表达式中执行的任务完成后,子线程结束。
4. 任务寻找到注册的后续任务,恢复异步方法的执行环境,继续执行后续任务,因为已经恢复到异步方法的执行上下文中,所以不存在跨线程的问题。
  看了这个过程,其实与我们使用ContinueWith的那种方式没什么太大的不同。回到上面的button1_Click方法,这下就好理解了,该方法从开始时同步运行,直至到达其第一个await表达式,此时异步的执行Task中指定的方法,然后将button1_Click方法挂起,回到button1_Click的调用者执行其他的代码;直到等待的任务完成后,回到button1_Click中继续执行后续的代码,也就是更新Label的内容。

  这里需要注意几点:
1. async和await只是上下文关键字。 当它们不修饰方法、lambda 表达式或匿名方法时,就不是关键字了,只作为普通的标识符。
2. 使用async修饰的异步方法的返回类型可以为 Task、Task<TResult> 或 void。 方法不能声明任何 ref 或 out 参数,但是可以调用具有这类参数的方法。
  如果异步方法需要一个 TResult 类型的返回值,则需要应指定 Task<TResult> 作为方法的返回类型。
  如果当方法完成时未返回有意义的值,则应使用 Task。 对于返回Task的异步方法,当 Task 完成时,任何等待 Task 的所有 await 表达式的计算结果都为 void。
  而使用void作为返回类型的方式主要是来定义事件处理程序,这些处理程序需要此返回类型。 使用void 作为异步方法的返回值时,该异步方法的调用方不能等待,并且无法捕获该方法引发的异常。
3. await表达式的返回值
  如果 await 应用于返回Task<TResult>的方法调用的结果,那么 await 表达式的类型是 TResult。 如果将 await 应用于返回Task的方法调用结果,则 await 表达式的类型无效。看下面的例子中的使用方式:

// 返回Task<TResult>的方法.
TResult result = await AsyncMethodThatReturnsTaskTResult(); // 返回一个Task的方法.
await AsyncMethodThatReturnsTask();

4.异常问题
  大多数异步方法返回 Task 或 Task<TResult>。 返回任务的属性承载有关其状态和历史记录的信息,例如任务是否已完成,异步方法是否引发异常或已取消,以及最终结果如何。 await 运算符会访问那些属性。
  如果任务返回异常,await 运算符会再次引发异常。
  如果任务被取消后返回,await 运算符也会再次引发 OperationCanceledException。
  总之,在await外围使用try/catch可以捕获任务中的异常。看一个例子:

public class AsyncTest
{
static void Main(string[] args)
{
AsyncTest c = new AsyncTest();
c.RunAsync(); // 模拟其他的工作
Thread.Sleep();
} public void RunAsync()
{
DisplayValue();
//这里不会阻塞
Console.WriteLine("RunAsync() End.");
} public Task<double> GetValueAsync(double num1, double num2)
{
return Task.Run(() =>
{
for (int i = ; i < ; i++)
{
num1 = num1 / num2; if (i == )
{
throw new Exception("Crash");
}
} return num1;
});
} public async void DisplayValue()
{
double result = ;
//此处会开新线程处理GetValueAsync任务,然后方法马上返回
try
{
result = await GetValueAsync(1234.5, 1.0);
}
catch (Exception)
{
//throw;
} //这之后的所有代码都会被封装成委托,在GetValueAsync任务完成时调用
Console.WriteLine("Value is : " + result);
}
}

  但是需要注意一点,如果任务抛出了多个异常(例如,该任务可能是启动了更多的子线程)时,await运算符只能抛出异常中的一个,而且不能确定是哪一个。这时就需要把这些子线程包装到一个Task中,这样这些异常就都会被包装到AggregateException中,看下面例子的做法:

public class AsyncTest
{
static void Main(string[] args)
{
AsyncTest c = new AsyncTest();
c.RunAsync(); // 模拟其他的工作
Thread.Sleep();
} public void RunAsync()
{
DisplayValue();
//这里不会阻塞
Console.WriteLine("RunAsync() End.");
} public async void DisplayValue()
{
Task all = null;
try
{
await (all = Task.WhenAll(
Task.Run(() => { throw new Exception("Ex1"); }),
Task.Run(() => { throw new Exception("Ex2"); }))
);
}
catch
{
foreach (var ex in all.Exception.InnerExceptions)
{
Console.WriteLine(ex.Message);
}
}
}
}

  当然了,大家也别忘了最后一招杀手锏:TaskScheduler.UnobservedTaskException,使用这个去捕获一些没有处理的异常。
  到此,异步方法就介绍到这里了。最后附上一位网上兄弟写的异步执行一些耗时操作的辅助类:

public static class TaskAsyncHelper
{
/// <summary>
/// 将一个方法function异步运行,在执行完毕时执行回调callback
/// </summary>
/// <param name="function">异步方法,该方法没有参数,返回类型必须是void</param>
/// <param name="callback">异步方法执行完毕时执行的回调方法,该方法没有参数,返回类型必须是void</param>
public static async void RunAsync(Action function, Action callback)
{
Func<System.Threading.Tasks.Task> taskFunc = () =>
{
return System.Threading.Tasks.Task.Run(() =>
{
function();
});
};
await taskFunc();
if (callback != null)
callback();
} /// <summary>
/// 将一个方法function异步运行,在执行完毕时执行回调callback
/// </summary>
/// <typeparam name="TResult">异步方法的返回类型</typeparam>
/// <param name="function">异步方法,该方法没有参数,返回类型必须是TResult</param>
/// <param name="callback">异步方法执行完毕时执行的回调方法,该方法参数为TResult,返回类型必须是void</param>
public static async void RunAsync<TResult>(Func<TResult> function, Action<TResult> callback)
{
Func<System.Threading.Tasks.Task<TResult>> taskFunc = () =>
{
return System.Threading.Tasks.Task.Run(() =>
{
return function();
});
};
TResult rlt = await taskFunc();
if (callback != null)
callback(rlt);
}
}

简单实用!

推荐链接:
你必须知道的异步编程:http://www.cnblogs.com/zhili/category/475336.html
传统异步编程指导:http://msdn.microsoft.com/zh-cn/library/vstudio/dd997423.aspx
使用async异步编程指导:http://msdn.microsoft.com/zh-cn/library/vstudio/hh191443.aspx

C#的变迁史 - C# 5.0 之并行编程总结篇的更多相关文章

  1. C#的变迁史 - C# 4.0 之线程安全集合篇

    作为多线程和并行计算不得不考虑的问题就是临界资源的访问问题,解决临界资源的访问通常是加锁或者是使用信号量,这个大家应该很熟悉了. 而集合作为一种重要的临界资源,通用性更广,为了让大家更安全的使用它们, ...

  2. C#的变迁史 - C# 5.0 之调用信息增强篇

    Caller Information CallerInformation是一个简单的新特性,包括三个新引入的Attribute,使用它们可以用来获取方法调用者的信息, 这三个Attribute在Sys ...

  3. C#的变迁史 - C# 4.0 之并行处理篇

    前面看完了Task对象,这里再看一下另一个息息相关的对象Parallel. Parallel对象 Parallel对象封装了能够利用多核并行执行的多线程操作,其内部使用Task来分装多线程的任务并试图 ...

  4. C#的变迁史 - C# 4.0篇

    C# 4.0 (.NET 4.0, VS2010) 第四代C#借鉴了动态语言的特性,搞出了动态语言运行时,真的是全面向“高大上”靠齐啊. 1. DLR动态语言运行时 C#作为静态语言,它需要编译以后运 ...

  5. C#的变迁史 - C# 4.0 之多线程篇

    在.NET 4.0中,并行计算与多线程得到了一定程度的加强,这主要体现在并行对象Parallel,多线程Task,与PLinq.这里对这些相关的特性一起总结一下. 使用Thread方式的线程无疑是比较 ...

  6. C#的变迁史 - C# 3.0篇

    C# 3.0 (.NET 3.5, VS2008) 第三代C#在语法元素基本完备的基础上提供了全新的开发工具和集合数据查询方式,极大的方便了开发. 1. WPF,WCF,WF 这3个工程类型奠定了新一 ...

  7. C#的变迁史 - C# 2.0篇

    在此重申一下,本文仅代表个人观点,如有不妥之处,还请自己辨别. 第一代的值类型装箱与拆箱的效率极其低下,特别是在集合中的表现,所以第二代C#重点解决了装箱的问题,加入了泛型.1. 泛型 - 珍惜生命, ...

  8. C#的变迁史 - C# 5.0 之其他增强篇

    1. 内置zip压缩与解压 Zip是最为常用的文件压缩格式之一,也被几乎所有操作系统支持.在之前,使用程序去进行zip压缩和解压要靠第三方组件去支持,这一点在.NET4.5中已有所改观,Zip压缩和解 ...

  9. C#的变迁史 - C# 1.0篇

    C#与.NET平台诞生已有10数年了,在每次重大的版本升级中,微软都为这门年轻的语言添加了许多实用的特性,下面我们就来看看每个版本都有些什么.老实说,分清这些并没什么太大的实际意义,但是很多老资格的. ...

随机推荐

  1. Redis教程(十一):虚拟内存介绍:

    转载于:http://www.itxuexiwang.com/a/shujukujishu/redis/2016/0216/138.html 一.简介: 和大多NoSQL数据库一样,Redis同样遵循 ...

  2. Js~(function(){})匿名自执行方法的作用

    匿名自执行方法体(function(){})经常用在设计JS插件上面,它定义相关组件的行为,自动初始化相关属性,而且在页面中可以直接执行,你不需要手动执行它,它被自动被执行! 在设计你的匿名自执行方法 ...

  3. Java线程:线程状态的转换

    Java线程:线程状态的转换   一.线程状态   线程的状态转换是线程控制的基础.线程状态总的可分为五大状态:分别是生.死.可运行.运行.等待/阻塞.用一个图来描述如下:   1.新状态:线程对象已 ...

  4. atitit..代码生成流程图 流程图绘制解决方案 java  c#.net  php v2

    atitit..代码生成流程图 流程图绘制解决方案 java  c#.net  php v2 1.1. Markdown 推荐,就是代码和flow都不能直接使用.1 1.2. Java code2fl ...

  5. Atitit.软件中见算法 程序设计五大种类算法

    Atitit.软件中见算法 程序设计五大种类算法 1. 算法的定义1 2. 算法的复杂度1 2.1. Algo cate2 3. 分治法2 4. 动态规划法2 5. 贪心算法3 6. 回溯法3 7. ...

  6. Atitit 异常的实现原理 与用户业务异常

    Atitit 异常的实现原理 与用户业务异常 1.1. 异常的实现原理1 1.2. 用户业务异常1 1.3. 异常转译和异常链2 1.4. 避免异常2 1.5. 异常恢复3 1.6. catch代码块 ...

  7. java 线程协作 wait(等待)与 notiy(通知)

    一.wait().notify()和notifyAll() 为了更好的支持多线程之间的协作,JDK提供了三个重要的本地方法 //调用某个对象的wait()方法能让当前线程阻塞,并且当前线程必须拥有此对 ...

  8. DOM_02之查找及元素操作

    1.查找之按节点间关系查找周围元素: 2.查找之HTML属性:①按id查找:var elem=document.getElementById("id"):找到一个元素,必须docu ...

  9. 转 - ubuntu apache2下目录结构

    ubuntu apache2下目录结构 原文:http://blog.csdn.net/jibcy/article/details/8060651 在Windows下,Apache的配置文件通常只有一 ...

  10. 开源IM工程“蘑菇街TeamTalk”的现状:一场有始无终的开源秀

    1.前言 随着云IM的发展,已吸引越来越多有IM需求的APP接入.但考虑到云IM无论从商业模式还是运营模式上,还需经过多年的沉淀,才可能真正实现客户与服务商的运营和服务良性循环的双赢局面.在此之前,加 ...