多线程和异步有什么关联和区别?如何实现异步?
很多很多年前,有个叫 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 可以开启有限个的线程,并对任务排队,仅当有线程空闲时,才会继续处理任务。BeginInvoke
BeginInvoke 是 Delegate 上的一个成员,它与 EndInvoke 一起使用可以实现异步操作。BeginInvoke 相当于上面例子中 Thread.Start() 的功能EndInvoke 相当于上面例子中 Thread.Join() 的功能
因为 BeginInvoke 是 Delegate 上的成员,所以我们先声明一个 Delegate/// /// 这是一个描述了一个使用整形并返回整形的委托类型。 /// 你可以使用直接使用 Func 来作为委托的类型。 /// /// /// 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" 根据需求传入,你也可以传 null IAsyncResult 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 类型的 Task private Task AsyncPower(int i) { // 返回一个任务 // 使用 Task.Run 相当于先创建了一个 Task // 再 Start return Task.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修改返回结果,使用 Task.Run 包装返回的结果为调用 Power 的代码前加上 await 关键字为有 await 关键字的方法加上 async 关键字
新的代码如下Task Power(int i) { return Task.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 task = UserService.AsyncGetUserId(conn, "Admin"); task.ContinueWith(t => { // 此时的 conn 已经被主线程释放了 UserService.DoSomethingWithConn(conn); }); }
你需要使用一些额外的代码来解决这些问题。并且这些代码不一定具备通用性,往往要具体问题具体分析。
因此在实际任务中,到底选择 同步 还是 异步 要视具体情况而定。
今天本文介绍了几种实现 异步 的方法,不能说它们之间谁比谁更好一点,各有优劣。
篇幅原因,将不再对几种方案进行对比,会在以后的文章中详细地介绍各自优劣。
全系标配手机车钥匙,秦Pro超越版520上线5月20日,比亚迪秦Pro超越版正式上市。整车外观动感时尚,配置实用超前,还搭配了堪称黑科技的手机NFC车钥匙,官方价位仅为7。98万9。98万。值得一提的是,在6月30日前订车可
哈弗F7销量再度破万累计销量突破18万4月,哈弗F7终端销售10,911辆,环比增长80,成为当月最热潮品SUV。迄今,哈弗F7累计销量已突破18万辆,并带动哈弗F系成为细分市场最具吸引力的SUV车型。疫情期间,哈弗F
新一代智跑品质制胜起亚正埋头赶上车市脉动。在竞争最为激烈的SUV市场,新一代智跑上市一年销量破10万辆月均销量保持在6000辆以上,成为东风悦达起亚旗下表现最出色的产品。其中,产品品质与产品力的持续
雕琢光影声色翼联EDUP1080P智能自动对焦摄像头上市报刊杂志跌落大众视野,书信短信已然明日黄花,数字网络将21世纪彻底点亮。走过缤纷静谧的图文时代,视频开启了人们遍览山河的上帝之眼,直播又让一场时光盛宴活灵活现。随着网络大幅提速升级
测试结果流出你想知道的刀片电池安全测试都在这2020年3月刀片电池以征服针刺测试而广为人知,动力电池的安全性也由此备受关注。很多消费者想要了解更多刀片电池的安全信息,除了针刺测试之外,刀片电池还接受过哪些测试?表现如何呢?我
六款车型同时首发恒大汽车务实再出发01hr造车新势力最大一匹黑马,恒大汽车迈出了坚实的一步。8月3日,恒大汽车6款新车同时全球首发,在汽车圈和金融界掀起一道道飓风。造车新势力已举步维艰?恒大汽车给出了有力的答案。百
新平台新实力第十代索纳塔正式上市近日,北京现代第十代索纳塔正式上市,共5款车型,售价区间为16。18万20。58万元。新车定位智慧运动美学中高级座驾。作为现代汽车新一代iGMP平台下的首款量产车型,第十代索纳塔将
造车驶入快车道!恒大健康更名为恒大汽车近来,恒大健康(0708。HK)可谓动作频频,利好不断。继大幅降低投资门槛至每手500股后,7月27日再发公告,公司名称更改为恒大汽车。分析认为,此举不仅标志着其主营业务已明确为新
7月新车SUV主导,轿车K5凯酷比亚迪汉EV备受关注继6月车市迎来第一波新车高峰后,7月各厂商再次集中火力推出多款新车备战成都车展。SUV是今年车市最热门的市场。据DaasAuto达示数据梳理,7月新车共计42款,其中19款全新车型
年味家乡味好运总相随翼联EDUP欢度元宵佳节红红火火过大年,欢欢喜喜闹元宵。2月26日,一场别开生面的元宵欢宴在翼联EDUP热烈举办。翼联舞台张灯结彩,来自五湖四海的翼家人踊跃报名,精心准备了各自的拿手好菜,团聚在一起,分享
4G无处不在为何还用4GWiFi4G无线路由器上网更简单5G小荷尖尖,4G已成标配,4GWiFi是何方神圣?宽带网络日益普及,4GLTE无处不在,为何还用4GWiFi?畅玩4GWiFi更省电上网时,耗电量主要跟手机的发射功率有关。而发射