async 和 await 出现在C# 5.0之后,给并行编程带来了不少的方便,特别是当在MVC中的Action也变成async之后,有点开始什么都是async的味道了。但是这也给我们编程埋下了一些隐患,有时候可能会产生一些我们自己都不知道怎么产生的Bug,特别是如果连线程基础没有理解的情况下,更不知道如何去处理了。那今天我们就来好好看看这两兄弟和他们的叔叔(Task)爷爷(Thread)们到底有什么区别和特点,本文将会对Thread 到 Task 再到 .NET 4.5的 async和 await,这三种方式下的并行编程作一个概括性的介绍包括:开启线程,线程结果返回,线程中止,线程中的异常处理等。

创建

  1. static void Main(){
  2. new Thread(Go).Start(); // .NET 1.0开始就有的
  3. Task.Factory.StartNew(Go); // .NET 4.0 引入了 TPL
  4. Task.Run(new Action(Go)); // .NET 4.5 新增了一个Run的方法
  5. }
  6.  
  7. public static void Go(){
  8. Console.WriteLine("我是另一个线程");
  9. }

  这里面需要注意的是,创建Thread的实例之后,需要手动调用它的Start方法将其启动。但是对于Task来说,StartNew和Run的同时,既会创建新的线程,并且会立即启动它。

线程池

  线程的创建是比较占用资源的一件事情,.NET 为我们提供了线程池来帮助我们创建和管理线程。Task是默认会直接使用线程池,但是Thread不会。如果我们不使用Task,又想用线程池的话,可以使用ThreadPool类。

  1. static void Main() {
  2. Console.WriteLine("我是主线程:Thread Id {0}", Thread.CurrentThread.ManagedThreadId);
  3. ThreadPool.QueueUserWorkItem(Go);
  4.  
  5. Console.ReadLine();
  6. }
  7.  
  8. public static void Go(object data) {
  9. Console.WriteLine("我是另一个线程:Thread Id {0}",Thread.CurrentThread.ManagedThreadId);
  10. }

传入参数

  1. static void Main() {
  2. new Thread(Go).Start("arg1"); // 没有匿名委托之前,我们只能这样传入一个object的参数
  3.  
  4. new Thread(delegate(){ // 有了匿名委托之后...
  5. GoGoGo("arg1", "arg2", "arg3");
  6. });
  7.  
  8. new Thread(() => { // 当然,还有 Lambada
  9. GoGoGo("arg1","arg2","arg3");
  10. }).Start();
  11.  
  12. Task.Run(() =>{ // Task能这么灵活,也是因为有了Lambda呀。
  13. GoGoGo("arg1", "arg2", "arg3");
  14. });
  15. }
  16.  
  17. public static void Go(object name){
  18. // TODO
  19. }
  20.  
  21. public static void GoGoGo(string arg1, string arg2, string arg3){
  22. // TODO
  23. }

返回值

  Thead是不能返回值的,但是作为更高级的Task当然要弥补一下这个功能。

  1. static void Main() {
  2. // GetDayOfThisWeek 运行在另外一个线程中
  3. var dayName = Task.Run<string>(() => { return GetDayOfThisWeek(); });
  4. Console.WriteLine("今天是:{0}",dayName.Result);
  5. }

共享数据

  上面说了参数和返回值,我们来看一下线程之间共享数据的问题。

  1. private static bool _isDone = false;
  2. static void Main(){
  3. new Thread(Done).Start();
  4. new Thread(Done).Start();
  5. }
  6.  
  7. static void Done(){
  8. if (!_isDone) {
  9. _isDone = true; // 第二个线程来的时候,就不会再执行了(也不是绝对的,取决于计算机的CPU数量以及当时的运行情况)
  10. Console.WriteLine("Done");
  11. }
  12. }

 

  线程之间可以通过static变量来共享数据。

线程安全

  我们先把上面的代码小小的调整一下,就知道什么是线程安全了。我们把Done方法中的两句话对换了一下位置 。

  1. private static bool _isDone = false;
  2. static void Main(){
  3. new Thread(Done).Start();
  4. new Thread(Done).Start();
  5. Console.ReadLine();
  6. }
  7.  
  8. static void Done(){
  9. if (!_isDone) {
  10. Console.WriteLine("Done"); // 猜猜这里面会被执行几次?
  11. _isDone = true;
  12. }
  13. }

  上面这种情况不会一直发生,但是如果你运气好的话,就会中奖了。因为第一个线程还没有来得及把_isDone设置成true,第二个线程就进来了,而这不是我们想要的结果,在多个线程下,结果不是我们的预期结果,这就是线程不安全。

  要解决上面遇到的问题,我们就要用到锁。锁的类型有独占锁,互斥锁,以及读写锁等,我们这里就简单演示一下独占锁。

  1. private static bool _isDone = false;
  2. private static object _lock = new object();
  3. static void Main(){
  4. new Thread(Done).Start();
  5. new Thread(Done).Start();
  6. Console.ReadLine();
  7. }
  8.  
  9. static void Done(){
  10. lock (_lock){
  11. if (!_isDone){
  12. Console.WriteLine("Done"); // 猜猜这里面会被执行几次?
  13. _isDone = true;
  14. }
  15. }
  16. }

  再我们加上锁之后,被锁住的代码在同一个时间内只允许一个线程访问,其它的线程会被阻塞,只有等到这个锁被释放之后其它的线程才能执行被锁住的代码。

Semaphore 信号量

  我实在不知道这个单词应该怎么翻译,从官方的解释来看,我们可以这样理解。它可以控制对某一段代码或者对某个资源访问的线程的数量,超过这个数量之后,其它的线程就得等待,只有等现在有线程释放了之后,下面的线程才能访问。这个跟锁有相似的功能,只不过不是独占的,它允许一定数量的线程同时访问。

  1. static SemaphoreSlim _sem = new SemaphoreSlim(3); // 我们限制能同时访问的线程数量是3
  2. static void Main(){
  3. for (int i = 1; i <= 5; i++) new Thread(Enter).Start(i);
  4. Console.ReadLine();
  5. }
  6.  
  7. static void Enter(object id){
  8. Console.WriteLine(id + " 开始排队...");
  9. _sem.Wait();
  10. Console.WriteLine(id + " 开始执行!");
  11. Thread.Sleep(1000 * (int)id);
  12. Console.WriteLine(id + " 执行完毕,离开!");
  13. _sem.Release();
  14. }

  

在最开始的时候,前3个排队之后就立即进入执行,但是4和5,只有等到有线程退出之后才可以执行。

异常处理

  其它线程的异常,主线程可以捕获到么?

  1. public static void Main(){
  2. try{
  3. new Thread(Go).Start();
  4. }
  5. catch (Exception ex){
  6. // 其它线程里面的异常,我们这里面是捕获不到的。
  7. Console.WriteLine("Exception!");
  8. }
  9. }
  10. static void Go() { throw null; }

  那么升级了的Task呢?

  1. public static void Main(){
  2. try{
  3. var task = Task.Run(() => { Go(); });
  4. task.Wait(); // 在调用了这句话之后,主线程才能捕获task里面的异常
  5.  
  6. // 对于有返回值的Task, 我们接收了它的返回值就不需要再调用Wait方法了
  7. // GetName 里面的异常我们也可以捕获到
  8. var task2 = Task.Run(() => { return GetName(); });
  9. var name = task2.Result;
  10. }
  11. catch (Exception ex){
  12. Console.WriteLine("Exception!");
  13. }
  14. }
  15. static void Go() { throw null; }
  16. static string GetName() { throw null; }

await 的原形

 await后的的执行顺序

  1. 进入主线程开始执行
  2. 调用async方法,返回一个Task,注意这个时候另外一个线程已经开始运行,也就是GetName方法已经开始工作了
  3. 另一个线程在获取名称
  4. 第3步和第4步是同时进行的,主线程并没有挂起等待
  5. 如果另一个线程已经执行完毕,name.IsCompleted=true,主线程仍然不用挂起,直接拿结果就可以了。如果另一个线程还同有执行完毕, name.IsCompleted=false,那么主线程会挂起等待,直到返回结果为止。

只有async方法在调用前才能加await么?

  1. static void Main(){
  2. Test();
  3. Console.ReadLine();
  4. }
  5.  
  6. static async void Test(){
  7. Task<string> task = Task.Run(() =>{
  8. Thread.Sleep(5000);
  9. return "Hello World";
  10. });
  11. string str = await task; //5 秒之后才会执行这里
  12. Console.WriteLine(str);
  13. }

  答案很明显:await并不是针对于async的方法,而是针对async方法所返回给我们的Task,这也是为什么所有的async方法都必须返回给我们Task。所以我们同样可以在Task前面也加上await关键字,这样做实际上是告诉编译器我需要等这个Task的返回值或者等这个Task执行完毕之后才能继续往下走。

不用await关键字,如何确认Task执行完毕了?

  1. static void Main(){
  2. var task = Task.Run(() =>{
  3. return GetName();
  4. });
  5.  
  6. task.GetAwaiter().OnCompleted(() =>{
  7. // 2 秒之后才会执行这里
  8. var name = task.Result;
  9. Console.WriteLine("My name is: " + name);
  10. });
  11.  
  12. Console.WriteLine("主线程执行完毕");
  13. Console.ReadLine();
  14. }
  15.  
  16. static string GetName(){
  17. Console.WriteLine("另外一个线程在获取名称");
  18. Thread.Sleep(2000);
  19. return "Jesse";
  20. }

Task.GetAwaiter()和await Task 的区别?

  • 加上await关键字之后,后面的代码都会被阻塞,直到task执行完毕有返回值的时候才会继续向下执行,这一段时间主线程会处于挂起状态。
  • GetAwaiter方法会返回一个awaitable的对象(继承了INotifyCompletion.OnCompleted方法)我们只是传递了一个委托进去,等task完成了就会执行这个委托,但是并不会阻塞主线程,下面的代码会立即执行。这也是为什么我们结果里面第一句话会是 “主线程执行完毕”!

Task如何阻塞主线程?

  上面的右边是属于没有阻塞的情况,和我们的await仍然有一点差别,那么在获取Task的结果前如何阻塞主线程呢?

  1. static void Main(){
  2. var task = Task.Run(() =>{
  3. return GetName();
  4. });
  5.  
  6. var name = task.GetAwaiter().GetResult();
  7. Console.WriteLine("My name is:{0}",name);
  8.  
  9. Console.WriteLine("主线程执行完毕");
  10. Console.ReadLine();
  11. }
  12.  
  13. static string GetName(){
  14. Console.WriteLine("另外一个线程在获取名称");
  15. Thread.Sleep(2000);
  16. return "Jesse";
  17. }

  

Task.GetAwait()方法会给我们返回一个awaitable的对象,通过调用这个对象的GetResult方法就会阻塞主线程,当然也不是所有的情况都会阻塞。还记得我们Task的特性么? 在一开始的时候就启动了另一个线程去执行这个Task,当我们调用它的结果的时候如果这个Task已经执行完毕,主线程是不用等待可以直接拿其结果的,如果没有执行完毕那主线程就得挂起等待了。

await 实质是在调用awaitable对象的GetResult方法

  1. static async Task Test(){
  2. Task<string> task = Task.Run(() =>{
  3. Console.WriteLine("另一个线程在运行!"); // 这句话只会被执行一次
  4. Thread.Sleep(2000);
  5. return "Hello World";
  6. });
  7.  
  8. // 这里主线程会挂起等待,直到task执行完毕我们拿到返回结果
  9. var result = task.GetAwaiter().GetResult();
  10. // 这里不会挂起等待,因为task已经执行完了,我们可以直接拿到结果
  11. var result2 = await task;
  12.  
  13. string str = await task; //5 秒之后才会执行这里
  14. Console.WriteLine(str);
  15. }

到此为止,await就真相大白了,欢迎点评。Enjoy Coding! :)

原文地址:http://www.cnblogs.com/jesse2013/p/async-and-await.html

[转载]async & await 的前世今生的更多相关文章

  1. 【转载】async & await 的前世今生(Updated)

    async 和 await 出现在C# 5.0之后,给并行编程带来了不少的方便,特别是当在MVC中的Action也变成async之后,有点开始什么都是async的味道了.但是这也给我们编程埋下了一些隐 ...

  2. async & await 的前世今生

    async 和 await 出现在C# 5.0之后,给并行编程带来了不少的方便,特别是当在MVC中的Action也变成async之后,有点开始什么都是async的味道了.但是这也给我们编程埋下了一些隐 ...

  3. async & await 的前世今生(Updated)----代码demo

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...

  4. async & await 的前世今生(Updated)

    async 和 await 出现在C# 5.0之后,给并行编程带来了不少的方便,特别是当在MVC中的Action也变成async之后,有点开始什么都是async的味道了.但是这也给我们编程埋下了一些隐 ...

  5. 【转】async & await 的前世今生

    async 和 await 出现在C# 5.0之后,给并行编程带来了不少的方便,特别是当在MVC中的Action也变成async之后,有点开始什么都是async的味道了.但是这也给我们编程埋下了一些隐 ...

  6. async await的前世今生

    async 和 await 出现在C# 5.0之后,给并行编程带来了不少的方便,特别是当在MVC中的Action也变成async之后,有点开始什么都是async的味道了.但是这也给我们编程埋下了一些隐 ...

  7. 【转】async & await 的前世今生(Updated)

    async 和 await 出现在C# 5.0之后,给并行编程带来了不少的方便,特别是当在MVC中的Action也变成async之后,有点开始什么都是async的味道了.但是这也给我们编程埋下了一些隐 ...

  8. [转]async & await 的前世今生(Updated)

    async 和 await 出现在C# 5.0之后,给并行编程带来了不少的方便,特别是当在MVC中的Action也变成async之后,有点开始什么都是async的味道了.但是这也给我们编程埋下了一些隐 ...

  9. Atitit. Async await 优缺点 异步编程的原理and实现 java c# php

    Atitit. Async await 优缺点 异步编程的原理and实现 java c# php 1. async & await的来源1 2. 异步编程history1 2.1. 线程池 2 ...

随机推荐

  1. Nginx - Additional Modules, Content and Encoding

    The following set of modules provides functionalities having an effect on the contents served to the ...

  2. Android 如何监听返回键,弹出一个退出对话框

    android 如何监听返回键点击事件,并创建一个退出对话框, 防止自己写的应用程序不小心点击退出键而直接退出.自己记录下这个简单的demo,备用. public class BackKeyTest ...

  3. Android Studio 创建aar包与引用

    两者区别:*.jar: 只包含了class文件与清单文件 ,不包含资源文件,如图片等所有res中的文件.*.aar: 包含所有资源 ,class以及res资源文件全部包含 一.创建aar包1.创建一个 ...

  4. OC2_点语法(属性关键字)

    // // Dog.h // OC2_点语法(属性关键字) // // Created by zhangxueming on 15/6/16. // Copyright (c) 2015年 zhang ...

  5. 项目中的那些事---JavaScript

    一.String.charAt(index) 作用:获取字符串指定索引位置的字符 注意:index的值是0~(字符串长度-1)之间的值 <script type="text/javas ...

  6. java新手笔记34 连接数据库

    1.JdbcUtil package com.yfs.javase.jdbc; import java.sql.Connection; import java.sql.DriverManager; i ...

  7. 4月10日学习笔记——jQuery选择器

    概念 jQuery 是一套Javascript脚本库,注意 jQuery 是脚本库,而不是脚本框架."库"不等于"框架".jQuery 并不能帮助我们解决脚本的 ...

  8. Bootstrap学习笔记(一) 排版

    Bootsrap是一款优秀的前端开发框架,我从慕课网上开始学习Bootstrap,以下我学习过程中的一些笔记及代码. 首先学习排版: 从Bootstrap网站下载Bootstrap3中文文档(V3.3 ...

  9. PDF编辑、删除、替换某页面或文字

    在工作中,我们常常会用到PDF,当然尤其是会计,我虽然是程序员,但是“小老鼠”是会计,前几天,突然问我,怎么样将PDF中的某个页面替换掉,也就是删掉某页然后再从另外一个地方找一页补上来: 还需要改变这 ...

  10. 关于Socket编写简单聊天工具的总结(原创)

    这段时间再看socket编程,虽然现在是刚刚接触,但是还是忍不住想写一篇总结,来激励自己努力学习,写的不好的地方,还请大家指教啊! 下面针对一个简单的发送消息和文件的程序说说吧.   首先是服务器需要 ...