.Net异步编程 z

1. 引言

最近在学习Abp框架,发现Abp框架的不少Api都提供了同步异步两种写法。异步编程提及来,你们可能都会说异步编程性能好。但好在哪里,引入了什么问题,以及如何使用,想必也未必能答的上来。
本身对异步编程也不是很了解,今天就以学习的目的,来梳理下同步异步编程的基础知识,而后再来介绍下如何使用async/await进行异步编程。下图是一张大纲,具体可查看脑图分享连接php

2. 同步异步编程

同步编程是对于单线程来讲的,就像咱们编写的控制台程序,以main方法为入口,顺序执行咱们编写的代码。
异步编程是对于多线程来讲的,经过建立不一样线程来实现多个任务的并行执行。编程

3. 线程

.Net 1.0就发布了System.Threading,其中提供了许多类型(好比Thread、ThreadStart等)能够显示的建立线程。
说到Thread,咱们须要了解如下几个概念:数组

3.1. 什么是主线程

每个Windows进程都刚好包含一个用做程序入口点的主线程。进程的入口点建立的第一个线程被称为主线程。.Net执行程序(控制台、Windows Form、Wpf等)使用Main()方法做为程序入口点。当调用该方法时,主线程被建立。多线程

3.2. 什么是工做者线程

由主线程建立的线程,能够称为工做者线程,用来去执行某项具体的任务。并发

3.3. 什么是前台线程

默认状况下,使用Thread.Start()方法建立的线程都是前台线程。前台线程能阻止应用程序的终结,只有全部的前台线程执行完毕,CLR才能关闭应用程序(即卸载承载的应用程序域)。前台线程也属于工做者线程。框架

3.4. 什么是后台线程

后台线程不会影响应用程序的终结,当全部前台线程执行完毕后,后台线程不管是否执行完毕,都会被终结。通常后台线程用来作些可有可无的任务(好比邮箱每隔一段时间就去检查下邮件,天气应用每隔一段时间去更新天气)。后台线程也属于工做者线程。异步

说了这么多概念不如来段代码:async

//主线程入口 static void Main(string[] args) { Console.WriteLine("主线程开始!"); //建立前台工做线程 Thread t1 = new Thread(Task1); t1.Start(); //建立后台工做线程 Thread t2= new Thread(new ParameterizedThreadStart(Task2)); t2.IsBackground = true;//设置为后台线程 t2.Start("传参"); } private static void Task1() { Thread.Sleep(1000);//模拟耗时操做,睡眠1s Console.WriteLine("前台线程被调用!"); } private static void Task2(object data) { Thread.Sleep(2000);//模拟耗时操做,睡眠2s Console.WriteLine("后台线程被调用!" + data); }

执行发现,【后台线程被调用】将不会显示。由于当全部的前台线程执行完毕后,应用程序就关闭了,不会等待全部的后台线程执行完毕,因此不会显示。异步编程

4. ThreadPool(线程池)

线程池是为忽然大量爆发的线程设计的,经过有限的几个固定线程为大量的操做服务,减小了建立和销毁线程所需的时间,从而提升效率,这也是线程池的主要好处。
ThreadPool适用于并发运行若干个任务且运行时间不长且互不干扰的场景。
还有一点须要注意,经过线程池建立的任务是后台任务。oop

举个例子:

//主线程入口 static void Main(string[] args) { Console.WriteLine("主线程开始!"); //建立要执行的任务 WaitCallback workItem = state => Console.WriteLine("当前线程Id为:" + Thread.CurrentThread.ManagedThreadId); //重复调用10次 for (int i = 0; i < 10; i++) { ThreadPool.QueueUserWorkItem(workItem); } Console.ReadLine(); }


从图中能够看出,程序并无每次执行任务都建立新的线程,而是循环利用线程池中维护的线程。
若是去掉最后一句Consoler.ReadLine(),会发现程序仅输出【主线程开始!】就直接退出,从而肯定ThreadPool建立的线程都是后台线程。

5. System.Threading.Tasks

.Net 4.0引入了System.Threading.Tasks,简化了咱们进行异步编程的方式,而不用直接与线程和线程池打交道。
System.Threading.Tasks中的类型被称为任务并行库(TPL)。TPL使用CLR线程池(说明使用TPL建立的线程都是后台线程)自动将应用程序的工做动态分配到可用的CPU中。

5.1. Parallel(数据并行)

数据并行是指使用Parallel.For()或Parallel.ForEach()方法以并行方式对数组或集合中的数据进行迭代。
看怎么用:

ParallelLoopResult result = Parallel.For(0, 10000, i => { Console.WriteLine("{0}, task: {1} , thread: {2}", i, Task.CurrentId, Thread.CurrentThread.ManagedThreadId); });

5.2. PLINQ(并行LINQ查询)

为并行运行而设计的LINQ查询为PLINQ。System.Linq命名空间的ParallelEnumerable中包含了一些扩展方法来支持PINQ查询。
使用举例:

int[] modThreeIsZero = (from num in source.AsParallel() where num % 3 == 0 orderby num descending select num).ToArray(); 

5.3. Task

Task,字面义,任务。使用Task类能够轻松地在次线程中调用方法。

static void Main(string[] args) { Console.WriteLine("主线程ID:" + Thread.CurrentThread.ManagedThreadId); Task.Factory.StartNew(() => Console.WriteLine("Task对应线程ID:" + Thread.CurrentThread.ManagedThreadId)); Console.ReadLine(); }


能够看见,使用Task咱们没必要理会具体线程的建立。
咱们也可使用.NET 4.5引入的Task.Run静态方法来启动一个线程。

static void Main(string[] args) { Console.WriteLine("主线程ID:" + Thread.CurrentThread.ManagedThreadId); Task.Run(() => Console.WriteLine("Task对应线程ID:" + Thread.CurrentThread.ManagedThreadId)); Console.ReadLine(); }

Task类提供了Wait()方法,用来等待线程task执行完毕。

5.4. Task

Task是Task的泛型版本,能够接收一个返回值。

static void Main(string[] args) { Console.WriteLine("主线程ID:" + Thread.CurrentThread.ManagedThreadId); Task<string> task = Task.Run(() => { return Thread.CurrentThread.ManagedThreadId.ToString(); }); Console.WriteLine("建立Task对应的线程ID:" + task.Result); Console.ReadLine(); }

Task提供了不少方法,帮助咱们进行异步任务。了解更多,可参考MSDN

5.5. async/await 特性

C# async关键字用来指定某个方法、Lambda表达式或匿名方法自动以异步的方式来调用。

我们先来看一个具体的示例吧。

static void Main(string[] args) { Console.WriteLine("主线程启动,当前线程为:" + Thread.CurrentThread.ManagedThreadId); Task<int> task = GetLengthAsync(); Console.WriteLine("回到主线程,当前线程为:" + Thread.CurrentThread.ManagedThreadId); Console.WriteLine("task的返回值是" + task.Result); Console.WriteLine("主线程结束,当前线程为:" + Thread.CurrentThread.ManagedThreadId); } static async Task<int> GetLengthAsync() { Console.WriteLine("GetLengthAsync()开始执行,当前线程为:" + Thread.CurrentThread.ManagedThreadId); var str = await GetStringAsync(); Console.WriteLine("GetLengthAsync()执行完毕,当前线程为:" + Thread.CurrentThread.ManagedThreadId); return str.Length; } static Task<string> GetStringAsync() { Console.WriteLine("-----------------"); Thread.Sleep(5000); Console.WriteLine("GetStringAsync()开始执行,当前线程为:" + Thread.CurrentThread.ManagedThreadId); return Task.Run(() => { Console.WriteLine("异步任务开始执行,当前线程为:" + Thread.CurrentThread.ManagedThreadId); Thread.Sleep(5000); Console.WriteLine("GetStringAsync()执行完毕,当前线程为:" + Thread.CurrentThread.ManagedThreadId); return "GetStringAsync()执行完毕"; }); }

是否是对执行结果感到惊讶?惊讶是对的,且听咱们下面娓娓道来。

  1. 被async标记的方法,意味着能够在方法内部使用await,这样该方法将会在一个await point(等待点)处被挂起,而且在等待的实例完成后该方法被异步唤醒。【注意:await point(等待点)处被挂起,并非说在代码中使用await SomeMethodAsync()处就挂起,而是在进入SomeMethodAsync()真正执行异步耗时任务时被挂起,切记,切记!!!】
  2. async标记的方法,返回值类型为voidTaskTask<T>
  3. 被async标记的方法,方法的执行结果或者任何异常都将直接反映在返回类型中。
  4. 不是被async标记的方法,就会被异步执行,刚开始都是同步开始执行。换句话说,方法被async标记不会影响方法是同步仍是异步的方式完成运行。事实上,async使得方法能被分解成几个部分,一部分同步运行,一些部分能够异步的运行(而这些部分正是使用await显示编码的部分),从而使得该方法能够异步的完成。
  5. await关键字告诉编译器在async标记的方法中插入一个可能的挂起/唤醒点。 逻辑上,这意味着当你写await someMethod();时,编译器将生成代码来检查someMethod()表明的操做是否已经完成。若是已经完成,则从await标记的唤醒点处继续开始同步执行;若是没有完成,将为等待的someMethod()生成一个continue委托,当someMethod()表明的操做完成的时候调用continue委托。这个continue委托将控制权从新返回到async方法对应的await唤醒点处。
    返回到await唤醒点处后,无论等待的someMethod()是否已经经完成,任何结果均可从Task中提取,或者若是someMethod()操做失败,发生的任何异常随Task一块儿返回或返回给SynchronizationContext

从第4点能够解释为何上面的demo当调用GetLengthAsync();方法时,输出GetLengthAsync()开始执行,当前线程为:1
从第1点能够解释调用await GetStringAsync();后,为何程序会继续同步执行输出GetStringAsync()开始执行,当前线程为:1,且会继续执行异步任务输出异步任务开始执行,当前线程为:3,而直到代码执行到Thread.Sleep(5000)才会返回到主线程输出回到主线程,当前线程为:1。这里Thread.Sleep(5000)就是await point(等待点)。
回到主线程后,由于要输出task.Result,因此主线程会等待。当异步任务完成后会输出GetStringAsync()执行完毕,当前线程为:3
从第5点能够解释,await等待异步任务完成后,GetLengthAsync()方法被异步唤醒,从而异步执行后续代码而输出GetLengthAsync()执行完毕,当前线程为:3

6. 总结

本文主要梳理了如下几点:

    1. 默认建立的Thread是前台线程,建立的Task为后台线程。
    2. ThreadPool建立的线程都是后台线程。
    3. 任务并行库(TPL)使用的是线程池技术。
    4. 调用async标记的方法,刚开始是同步执行的,只有当执行到await标记的方法中的异步耗时任务时,才会挂起。
相关文章
相关标签/搜索