很多很多年前,有个叫 DOS 的操作系统。
DOS 通过一行一行命令运行程序。在同一时候里,你只可能运行一个程序,这便是 单进程系统。
后来涌现了 Windows,用户可以在系统中打开多个程序并利用它们。这便是 多进程系统。

线程 与 进程 的关系,就犹如 进程 与 系统 的关系。一个 系统 可以存在多个 进程 ,一个 进程 也可以存在多个 线程 。
本日的主题与 多线程 的事理关系不大,因此就不在其 事理 上进行更多的解释和解释了。
什么是单线程,什么是多线程还得记大约五、六年前,我去 KFC 和 McDonald's 就创造了一个有趣的差异。
在 KFC 中,收银 与 配餐 是同一人。
顾客在点餐后,连续站在原地等待西餐,造成了 KFC 中常常能见到长长的军队在排队。
在 McDonald's ,这两件事是由两个不同的人卖力的。
顾客在点餐完成后,带着号离开点餐的军队,直接去等待配餐叫号。点餐的军队人数比 KFC 明显少了许多。
比拟这两种模式,你会创造
KFC 的模式很随意马虎积压一长排的顾客,让他们烦于等待而终极离开。McDonald's 的模式不随意马虎产生排长队的顾客,并且可以让顾客早早的进入叫号等餐环节。我们把 线程 视作 员工 ,把 顾客 视作 任务,于是这两个例子就可以非常形象的阐明什么 单线程 ,什么是 多线程 。
KFC 这种模式模式便是 单线程 , 一个任务从头至尾由一 线程 完成,在一个任务完成之前,不接管第二个任务。McDonald's 这种模式便是 多线程 , 将一个任务拆分身分歧的阶段(部分),并交给不同的 线程 分别处理。什么是同步,什么是异步当你明白了 KFC 和 McDonald's 后,统统就很大略了。
线程 是 员工,同步 / 异步 便是顾客点餐的流程了。
顾客不能去一下洗手间,只能呆呆地站在那里等待配置的模式便是 同步 。顾客支付往后,利用等待配置的韶光去一下洗手间,找个座位的模式便是 异步 。显而易见,异步 可以供应更高的效率,它可以利用 等待 的韶光去完成一些事情。
在我们的代码中,一个 顾客 一边等待配置、一边做些别的事情,便是 多线程 了。
因此,(单/多线程) 与 (同/异)步 是密不可分的两个观点。
实现异步在正常情形下,我们写出来的代码都是 同步 的,上一行代码没做完就肯定不会实行第二行。
以是 如何实现同步 这个问题的答案与 如何写一段代码 是一样的。
那么,我们自然而然的就把目光放在了 如何实现异步,这一话题上了。
在 .Net 中,我们有几种 异步 的实现方案 :
ThreadBeginInvokeTaskasync / await下面,我会先容每种方案是如何实现的
Thread首先,如上面所提到的,异步 的目标便是,先开始 某个任务,然后利用等待的韶光去做点 别的事情。
很明显,这里有两个线程
一个卖力 某个任务。另一个卖力 别的事情,并在完成 别的事情 后开始等待 某个任务 的完成。利用这个思想,我们可以自己做一个异步的小例子了。
// 某个任务的结果int resultOfSomeTask = 0;Thread someTask = new Thread(new ThreadStart(() =>{ Thread.Sleep(1000); resultOfSomeTask = 100;}));someTask.Start();DoSomething(1); // 做一些别的事情DoSomething(2); // 做一些别的事情DoSomething(3); // 做一些别的事情// 每隔一下子去看看 【某个任务】 是否完成了while (true){ if (someTask.ThreadState == ThreadState.Stopped) break; Thread.Sleep(1);}Assert.AreEqual(100, resultOfSomeTask);
代码解释
我们利用 Thread 创建一个线程,让它去完成 某个任务,我们仿照该任务须要 1秒钟,并会产生一个 100 的结果利用 Thread.Start 开始这个任务在 someTask 实行过程中,我们造作一些 别的事情利用一个轮询查看 someTask 的状态,当完成后,我们创造已经得到了 100 这个结果。上面例子中 while(true) 部分,我们可以利用 Thread.Join() 方法来代替以达到同样的效果。
// 某个任务的结果int resultOfSomeTask = 0;Thread someTask = new Thread(new ThreadStart(() =>{ Thread.Sleep(1000); resultOfSomeTask = 100;}));someTask.Start();DoSomeThine(2); // 做一些别的事情DoSomeThine(3); // 做一些别的事情DoSomeThine(1); // 做一些别的事情// 产生与 while(true) 同样的效果// 当 someTask 完成后,才会连续进行someTask.Join();Assert.AreEqual(100, resultOfSomeTask);
这种异步的实现办法让开发者无法只关注在 逻辑 本身,代码中混入了大量的与线程有关的代码。
而且最不人性化的是,Thread 要么没有参数,要么只给一个 object 类型的参数,最草稿的是,它 无法返回结果,我们必须写一些额外的代码、逻辑去要主线程中得到子线程中的结果。
题外话
在实际生产环境中,我们每每利用 ThreadPool 来开启线程。
毕竟每开一个线程,系统都会产生一相应的花费来支持它。
ThreadPool 可以开启有限个的线程,并对任务排队,仅当有线程空闲时,才会连续处理任务。
BeginInvokeBeginInvoke 是 Delegate 上的一个成员,它与 EndInvoke 一起利用可以实现异步操作。
BeginInvoke 相称于上面例子中 Thread.Start() 的功能EndInvoke 相称于上面例子中 Thread.Join() 的功能由于 BeginInvoke 是 Delegate 上的成员,以是我们先声明一个 Delegate
/// <summary>/// 这是一个描述了一个利用整形并返回整形的委托类型。/// 你可以利用直策应用 Func<int,int> 来作为委托的类型。/// </summary>/// <param name="i"></param>/// <returns></returns>public delegate int TaskGetIntByInt(int i);
BeginInvoke 的入参比较特殊,它分为两个分部。
前面的几个参数,便是委托中定义的参数后面两个参数,一个是异步任务完成时的回调,一个是可以向回调函数传入的额外参数,你可以通报任何你须要的内容至回调里,而避免了在进程内访问进程外成员的情形下面是一个 BeginInvoke 的例子
// 这是一个耗时1秒的任务,会返回入参的平方数TaskGetIntByInt someTask = new TaskGetIntByInt(i =>{ Thread.Sleep(1000); return i i;});// 定义一个函数,用于 someTask 完成时的回调AsyncCallback callback = new AsyncCallback(ar =>{ string state = ar.AsyncState as string; Assert.AreEqual("Hello", state);});// 开始平方数运算的任务// callback, "HelloWorld" 根据需求传入,你也可以传 nullIAsyncResult ar = someTask.BeginInvoke(10, callback, "HelloWorld");// 开始一些别的任务DoSomeThing(1);DoSomeThing(2);DoSomeThing(3);// 等待 someTask 的运算结果,形如 Thread.Join()int result = someTask.EndInvoke(ar);Assert.AreEqual(100, result);
代码解释
首先 创建委托的实例,你可以利用其它类型上的成员来布局,也可以像示例中那样直接写一个内部方法。
接下来 利用 BeginInvoke 开始异步调用。 把稳 这里返回了一个 IAsyncResult 类型。
你可以把这个 IAsyncResult 理解为你在 McDonald's 点好餐后的 号 , 每个人的 号 都是不同的,每个顾客都可以用这个 号 领取你的美食。
在代码中,每次调用 BeginInvoke 都会产生不同的 IAsyncResult,你可以用不同的 IAsyncResult 去获取它们对应的结果。
BeginInvoke 的时候,你还可以指定一个 回调函数 ,还可以指定一个变量,供 回调函数 利用。
此时,someTask 已经在子线程中运行了。同时,主线程连续实行了 3 个 DoSomething() 方法。
当你须要 someTask 的运行结果时,你只须要调用 someTask.EndInvoke(IAsyncResult) 。
当子线程已经完成后,调用 EndInvoke 你可以立即得到结果。当子线程尚未完成时,调用 EndInvoke 会一贯等待,等到子线程实行完成后,才可以得到结果。题外话
若你的异步任务是一个耗时极长的任务,在主线程利用 EndInvoke 会 傻等 良久。
此时,你可以将 EndInvoke 方法在 Callback 内实行。
将 someTask 作为 回调函数 的参数传入,就可以在 Callback 内利用 EndInvoke 得到结果。
TaskGetIntByInt someTask = new TaskGetIntByInt(i =>{ Thread.Sleep(3000); return i i;});DoSomeThing(1);DoSomeThing(2);DoSomeThing(3);AsyncCallback callback = new AsyncCallback(_ar =>{ // BeginInvoke 的末了一位参数可以通过 AsyncState 取得 TaskGetIntByInt task = (TaskGetIntByInt)_ar.AsyncState; int result = task.EndInvoke(_ar); Assert.AreEqual(100, result);});IAsyncResult ar = someTask.BeginInvoke(10, callback, someTask);
对付一个 异步 任务的结果,我们每每有两种方法处理 :
在主线程中等待结果在子线程中处理结果对付一个耗时较短的任务,我们可以先利用 异步 将该任务放在子线程中实行。再连续在主线程中处理其它任务,末了等待 异步 任务的完成。这种办法便是 在主线程中等待结果 。
对付一个耗时较长的任务,如果在主线程中 等待 会有可能对终端用户带来不好的运用体验。
因此,我们不会在主线程中等待 异步 的完成。
我们在 异步 任务开启后,就可以早早关照用户 你的任务正在处理中。同时在子线程中,当任务完成后,你可以利用数据库等手段,将 正在处理中的任务 标为 已完成,并关照他们。
Task从 .Net4.0 开始,Task 成为了实现 异步 的紧张利器。
Task 的用法与 JavaScript 中的 Promise 非常靠近。
Task 表示一个 异步 任务。废话不多说,我们先写一个返回 Task 的方法。
// Task<int> 表示这是一个返回 int 类型的 Taskprivate Task<int> AsyncPower(int i){ // 返回一个任务 // 利用 Task.Run 相称于先创建了一个 Task // 再 Start return Task<int>.Run(() => { // 该任务会耗时 1 秒钟 Thread.Sleep(1000); // 1 秒钟后会返回参数的平方 return i i; });}
与之条件到的相同,我们有两种方法处理这个 Task 的结果 :
在主线程中等待结果在子线程中处理结果我们看看两种模式分别是如何实现的
在主线程中等待结果直接访问 Task.Result 属性,就可以等待并得到 异步 任务的结果。
var task = AsyncPower(10);// 这里会等 1 秒int result = task.Result; // result = 100
怎么样 ? 是不是超级大略 ?
在子线程中处理结果利用方法 ContinueWith 可以添加一个方法,在 Task 完成后被实行。
这个 ContinueWith 和 JavaScript 里的 Promise 的 then 方法有着异曲同工的效果。
var task = AsyncPower(10);task.ContinueWith(t => { int result = t.Result; // result = 100});
怎么样 ? 是不是依然超级大略 ?
就像之前说的,Task 用起来就像 Promise,Promise 最大的特点便是可以用一步一步 then 下去。
$.get("someurl") .then(rlt => foo(rlt)) .then(rlt => bar(rlt));
Task 的 ContinueWith 也支持这样的编写方法 :
var task = AsyncPowe(10);task.ContinueWith(t => { // some code here}).ContinueWith(t => { // some code here}).CondinueWith(t => { // some code here});
async / await
这个 .Net 4.5 加入的关键字,让 异步代码 写起来和 同步代码 没什么差异了。
我们先看看下面的同步代码
int Power(int i){ return i i;}void Main(){ int result = Power(10); Console.WriteLine(result); // 100 Console.ReadLine();}
把上面的代码改成异步代码,只须要几个小小的改动 :
将 Power 的返回值改为 Task<T>修正返回结果,利用 Task.Run 包装返回的结果为调用 Power 的代码前加上 await 关键字为有 await 关键字的方法加上 async 关键字新的代码如下
Task<int> Power(int i){ return Task<int>.Run(()=> { Thread.Sleep(100); // 仿照耗时 return i i; });}async void Main(){ Console.WriteLine("Hello"); int result = await Power(10); Console.WriteLine(result); // 100 Console.ReadLine();}
运行一下,创造没什么差异。
如果你向掌握台输出线程ID的话,你会创造 Console.WriteLine("Hello") 和 Console.WriteLine(result) 并不事情在同一个线程同。
为什么会有这样的效果 ?
由于 编译器,你会创造,和之前的异步实现不同,async 和 await 不是某个封装了繁芜逻辑的类型,而是两个关键字。
关键字的意义便是编译过程。
在编译时,碰着 async 就会知道这是一个存在着异步的方法,编译器向这个类型添加一些额外的成员来支持 await 操作。
当碰着 await 关键字时,编译器会从当前行截断,并向后面的代码编译到 Task.ContinueWith 中去。
这样一来,看似同步的代码,经编译后,就会一拆为二。
前部分运行在主线程中,后部分运行在子线程中,分割点便是 await 所在的代码行。
慎用异步几种在 .Net 平台中利用 异步 的方法都先容完了,希望大家能够对 异步 编程有了一定的理解和认识。
但是,在实际生产中,依赖要慎用异步。
异步 在带来性能提高的同时,还会带来一些更繁芜的问题:
线程安全线程间的切换并不是有着类似 事务 的特色,它无法担保两个线程对同一资源的读写的完全性。
而且大部分情形下,一个线程操作完,就会被挂机实行另一个线程,以是对付多个线程访问同一资源,须要考虑线程安全的问题。
换句话说,便是担保一个线程在实行一个最小操作时,另一个线程不许可操作该工具。
调试难异步 的实质便是 多线程 ,当你考试测验用断点调试代码时,由于两个线程都在你的代码中运行,因此常常涌现从这个线程的断点进入另一个线程的断点的情景。
须要依赖 IDE 中更多的工具和设置,才能办理上述的问题。
分歧一的高下文异步 代码每每在子线程中运行。
子线程 很可能会利用在 主线程 中已经施放的资源。
比如
using(var conn = new SqlConnection("........")){ conn.Open(); // 假定一个根据用户名查询用户ID的方法 Tast<int> task = UserService.AsyncGetUserId(conn, "Admin"); task.ContinueWith(t => { // 此时的 conn 已经被主线程开释了 UserService.DoSomethingWithConn(conn); });}
你须要利用一些额外的代码来办理这些问题。并且这些代码不一定具备通用性,每每要详细问题详细剖析。
因此在实际任务中,到底选择 同步 还是 异步 要视详细情形而定。
本日本文先容了几种实现 异步 的方法,不能说它们之间谁比谁更好一点,各有利害。
篇幅缘故原由,将不再对几种方案进行比拟,会在往后的文章中详细地先容各自利害。