多线程和异步有什么关联和区别?如何实现异步?
很多很多年前,有个叫DOS的操作系统。
DOS通过一行一行命令运行程序。在同一时刻里,你只可能运行一个程序,这就是单进程系统。
后来出现了Windows,用户可以在系统中打开多个程序并使用它们。这就是多进程系统。
线程与进程的关系,就如同进程与系统的关系。一个系统可以存在多个进程,一个进程也可以存在多个线程。
今天的主题与多线程的原理关系不大,因此就不在其原理上进行更多的说明和解释了。什么是单线程,什么是多线程
还得记大约五、六年前,我去KFC和McDonalds就发现了一个有趣的区别。
在KFC中,收银与配餐是同一人。
顾客在点餐后,继续站在原地等待西餐,造成了KFC中常常能见到长长的队伍在排队。
在McDonalds,这两件事是由两个不同的人负责的。
顾客在点餐完成后,带着号离开点餐的队伍,直接去等待配餐叫号。点餐的队伍人数比KFC明显少了许多。
对比这两种模式,你会发现KFC的模式很容易积压一长排的顾客,让他们烦于等待而最终离开。McDonalds的模式不容易产生排长队的顾客,并且可以让顾客早早的进入叫号等餐环节。
我们把线程视作员工,把顾客视作任务,于是这两个例子就可以非常形象的解释什么单线程,什么是多线程。KFC这种模式模式就是单线程,一个任务从头至尾由一线程完成,在一个任务完成之前,不接受第二个任务。McDonalds这种模式就是多线程,将一个任务拆分成不同的阶段(部分),并交给不同的线程分别处理。什么是同步,什么是异步
当你明白了KFC和McDonalds后,一切就很简单了。
线程是员工,同步异步就是顾客点餐的流程了。顾客不能去一下洗手间,只能呆呆地站在那里等待配置的模式就是同步。顾客支付以后,利用等待配置的时间去一下洗手间,找个座位的模式就是异步。
显而易见,异步可以提供更高的效率,它可以利用等待的时间去完成一些事情。
在我们的代码中,一个顾客一边等待配置、一边做些别的事情,就是多线程了。
因此,(单多线程)与(同异)步是密不可分的两个概念。实现异步
在正常情况下,我们写出来的代码都是同步的,上一行代码没做完就肯定不会执行第二行。
所以如何实现同步这个问题的答案与如何写一段代码是一样的。
那么,我们自然而然的就把目光放在了如何实现异步,这一话题上了。
在。Net中,我们有几种异步的实现方案:ThreadBeginInvokeTaskasyncawait
下面,我会介绍每种方案是如何实现的Thread
首先,如上面所提到的,异步的目标就是,先开始某个任务,然后利用等待的时间去做点别的事情。
很明显,这里有两个线程一个负责某个任务。另一个负责别的事情,并在完成别的事情后开始等待某个任务的完成。
利用这个思想,我们可以自己做一个异步的小例子了。某个任务的结果intresultOfSomeTask0;ThreadsomeTasknewThread(newThreadStart((){Thread。Sleep(1000);resultOfSomeTask100;}));someTask。Start();DoSomething(1);做一些别的事情DoSomething(2);做一些别的事情DoSomething(3);做一些别的事情每隔一会儿去看看【某个任务】是否完成了while(true){if(someTask。ThreadStateThreadState。Stopped)break;Thread。Sleep(1);}Assert。AreEqual(100,resultOfSomeTask);
代码说明我们利用Thread创建一个线程,让它去完成某个任务,我们模拟该任务需要1秒钟,并会产生一个100的结果使用Thread。Start开始这个任务在someTask执行过程中,我们做作一些别的事情利用一个轮询查看someTask的状态,当完成后,我们发现已经得到了100这个结果。
上面例子中while(true)部分,我们可以使用Thread。Join()方法来代替以达到同样的效果。某个任务的结果intresultOfSomeTask0;ThreadsomeTasknewThread(newThreadStart((){Thread。Sleep(1000);resultOfSomeTask100;}));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上的成员,所以我们先声明一个Delegatesummary这是一个描述了一个使用整形并返回整形的委托类型。你可以使用直接使用Funcint,int来作为委托的类型。summaryparamnameiparamreturnsreturnspublicdelegateintTaskGetIntByInt(inti);
BeginInvoke的入参比较特别,它分为两个分部。前面的几个参数,就是委托中定义的参数后面两个参数,一个是异步任务完成时的回调,一个是可以向回调函数传入的额外参数,你可以传递任何你需要的内容至回调里,而避免了在进程内访问进程外成员的情况
下面是一个BeginInvoke的例子这是一个耗时1秒的任务,会返回入参的平方数TaskGetIntByIntsomeTasknewTaskGetIntByInt(i{Thread。Sleep(1000);returnii;});定义一个函数,用于someTask完成时的回调AsyncCallbackcallbacknewAsyncCallback(ar{stringstatear。AsyncStateasstring;Assert。AreEqual(Hello,state);});开始平方数运算的任务callback,HelloWorld根据需求传入,你也可以传nullIAsyncResultarsomeTask。BeginInvoke(10,callback,HelloWorld);开始一些别的任务DoSomeThing(1);DoSomeThing(2);DoSomeThing(3);等待someTask的运算结果,形如Thread。Join()intresultsomeTask。EndInvoke(ar);Assert。AreEqual(100,result);
代码说明
首先创建委托的实例,你可以使用其它类型上的成员来构造,也可以像示例中那样直接写一个内部方法。
接下来使用BeginInvoke开始异步调用。注意这里返回了一个IAsyncResult类型。
你可以把这个IAsyncResult理解为你在McDonalds点好餐后的号,每个人的号都是不同的,每个顾客都可以用这个号领取你的美食。
在代码中,每次调用BeginInvoke都会产生不同的IAsyncResult,你可以用不同的IAsyncResult去获取它们对应的结果。
BeginInvoke的时候,你还可以指定一个回调函数,还可以指定一个变量,供回调函数使用。
此时,someTask已经在子线程中运行了。同时,主线程继续执行了3个DoSomething()方法。
当你需要someTask的运行结果时,你只需要调用someTask。EndInvoke(IAsyncResult)。当子线程已经完成后,调用EndInvoke你可以立即得到结果。当子线程尚未完成时,调用EndInvoke会一直等待,等到子线程执行完成后,才可以得到结果。
题外话
若你的异步任务是一个耗时极长的任务,在主线程使用EndInvoke会傻等很久。
此时,你可以将EndInvoke方法在Callback内执行。
将someTask作为回调函数的参数传入,就可以在Callback内使用EndInvoke得到结果。TaskGetIntByIntsomeTasknewTaskGetIntByInt(i{Thread。Sleep(3000);returnii;});DoSomeThing(1);DoSomeThing(2);DoSomeThing(3);AsyncCallbackcallbacknewAsyncCallback(ar{BeginInvoke的最后一位参数可以通过AsyncState取得TaskGetIntByInttask(TaskGetIntByInt)ar。AsyncState;intresulttask。EndInvoke(ar);Assert。AreEqual(100,result);});IAsyncResultarsomeTask。BeginInvoke(10,callback,someTask);
对于一个异步任务的结果,我们往往有两种方法处理:在主线程中等待结果在子线程中处理结果
对于一个耗时较短的任务,我们可以先利用异步将该任务放在子线程中执行。再继续在主线程中处理其它任务,最后等待异步任务的完成。这种方式就是在主线程中等待结果。
对于一个耗时较长的任务,如果在主线程中等待会有可能对终端用户带来不好的应用体验。
因此,我们不会在主线程中等待异步的完成。
我们在异步任务开启后,就可以早早通知用户你的任务正在处理中。同时在子线程中,当任务完成后,你可以利用数据库等手段,将正在处理中的任务标为已完成,并通知他们。Task
从。Net4。0开始,Task成为了实现异步的主要利器。
Task的用法与JavaScript中的Promise非常接近。
Task表示一个异步任务。废话不多说,我们先写一个返回Task的方法。Taskint表示这是一个返回int类型的TaskprivateTaskintAsyncPower(inti){返回一个任务使用Task。Run相当于先创建了一个Task再StartreturnTaskint。Run((){该任务会耗时1秒钟Thread。Sleep(1000);1秒钟后会返回参数的平方returnii;});}
与之前提到的相同,我们有两种方法处理这个Task的结果:在主线程中等待结果在子线程中处理结果
我们看看两种模式分别是如何实现的在主线程中等待结果
直接访问Task。Result属性,就可以等待并得到异步任务的结果。vartaskAsyncPower(10);这里会等1秒intresulttask。Result;result100
怎么样?是不是超级简单?在子线程中处理结果
使用方法ContinueWith可以添加一个方法,在Task完成后被执行。
这个ContinueWith和JavaScript里的Promise的then方法有着异曲同工的效果。vartaskAsyncPower(10);task。ContinueWith(t{intresultt。Result;result100});
怎么样?是不是依然超级简单?
就像之前说的,Task用起来就像Promise,Promise最大的特点就是可以用一步一步then下去。。get(someurl)。then(rltfoo(rlt))。then(rltbar(rlt));
Task的ContinueWith也支持这样的编写方法:vartaskAsyncPowe(10);task。ContinueWith(t{somecodehere})。ContinueWith(t{somecodehere})。CondinueWith(t{somecodehere});asyncawait
这个。Net4。5加入的关键字,让异步代码写起来和同步代码没什么区别了。
我们先看看下面的同步代码intPower(inti){returnii;}voidMain(){intresultPower(10);Console。WriteLine(result);100Console。ReadLine();}
把上面的代码改成异步代码,只需要几个小小的改动:将Power的返回值改为Task修改返回结果,使用Task。Run包装返回的结果为调用Power的代码前加上await关键字为有await关键字的方法加上async关键字
新的代码如下TaskintPower(inti){returnTaskint。Run((){Thread。Sleep(100);模拟耗时returnii;});}asyncvoidMain(){Console。WriteLine(Hello);intresultawaitPower(10);Console。WriteLine(result);100Console。ReadLine();}
运行一下,发现没什么区别。
如果你向控制台输出线程ID的话,你会发现Console。WriteLine(Hello)和Console。WriteLine(result)并不工作在同一个线程同。
为什么会有这样的效果?
因为编译器,你会发现,和之前的异步实现不同,async和await不是某个封装了复杂逻辑的类型,而是两个关键字。
关键字的意义就是编译过程。
在编译时,遇到async就会知道这是一个存在着异步的方法,编译器向这个类型添加一些额外的成员来支持await操作。
当遇到await关键字时,编译器会从当前行截断,并向后面的代码编译到Task。ContinueWith中去。
这样一来,看似同步的代码,经编译后,就会一拆为二。
前部分运行在主线程中,后部分运行在子线程中,分割点就是await所在的代码行。慎用异步
几种在。Net平台中使用异步的方法都介绍完了,希望大家能够对异步编程有了一定的了解和认识。
但是,在实际生产中,依赖要慎用异步。
异步在带来性能提高的同时,还会带来一些更复杂的问题:线程安全
线程间的切换并不是有着类似事务的特征,它无法保证两个线程对同一资源的读写的完整性。
而且大部分情况下,一个线程操作完,就会被挂机执行另一个线程,所以对于多个线程访问同一资源,需要考虑线程安全的问题。
换句话说,就是保证一个线程在执行一个最小操作时,另一个线程不允许操作该对象。调试难
异步的本质就是多线程,当你尝试用断点调试代码时,由于两个线程都在你的代码中运行,因此常常出现从这个线程的断点进入另一个线程的断点的情景。
需要依赖IDE中更多的工具和设置,才能解决上述的问题。不统一的上下文
异步代码往往在子线程中运行。
子线程很可能会使用在主线程中已经施放的资源。
比如using(varconnnewSqlConnection(。。。。。。。。)){conn。Open();假定一个根据用户名查询用户ID的方法TastinttaskUserService。AsyncGetUserId(conn,Admin);task。ContinueWith(t{此时的conn已经被主线程释放了UserService。DoSomethingWithConn(conn);});}
你需要使用一些额外的代码来解决这些问题。并且这些代码不一定具备通用性,往往要具体问题具体分析。
因此在实际任务中,到底选择同步还是异步要视具体情况而定。
今天本文介绍了几种实现异步的方法,不能说它们之间谁比谁更好一点,各有优劣。
篇幅原因,将不再对几种方案进行对比,会在以后的文章中详细地介绍各自优劣。