异步IO与轻量级线程

线程通常用来执行并行计算,为大量阻塞操作增加并发度。

计算机执行计算任务时,通常还需要与各种IO设备交互,这些设备和CPU、内存比起来慢几个数量级。大部分web应用的瓶颈都在于IO操作,如读写磁盘,网络操作,读取数据库。使用怎样的策略等待这段时间,就成了改善性能的关键。

线程怎么样?

如果大量创建线程,在线程数大于CPU数时,线程会有额外的调度开销,所以线程数太多反而会使计算性能下降。而且多线程又免不了就需要加锁。程序自己维护锁的状态,开发成本很高而且还不易调试。

基于回调的异步IO

这些时候,除了创建大量线程以外,还可以使用异步IO来完成同样的功能。如node.js,就是将所有的IO操作强制异步进行。异步IO原理上很简单,用户线程某一刻发起一个请求(对于IO来说通常只有读和写2种),在IO设备准备好的时候会回调用户指定的过程,异步IO并不阻塞用户线程,可以使用少量线程完成相同的功能。异步IO关键的优势在于,它去掉了处理单个请求中那些无意义的等待时间。虽然单位时间内处理的请求没有变化,但是每个请求的处理时间却减少了。不过异步IO用多了一点也不美,由于它要求程序必须遵从请求/回调的方式,所以一个完整的过程可能被分割为多个片断,程序架构非常难以控制。这种情况叫做回调地狱。

既不用基于回调的异步IO,又不大量使用线程,这时候可以引入轻量级线程解决。


轻量级线程
轻量级线程实际上是伪线程,它是由用户来控制“线程”切换的。当一个操作需要等待异步完成时,它告诉调度器,暂停自己的执行,切换到另一个任务。当IO事件发生时,再从该断点恢复执行。在windows上,系统提供了fiber,Linux上也可以使用ucontext轻松地实现协程Python也实现了轻量级线程,著名的网络游戏EVE服务端就是使用它来实现的。其它一些语言的continuation机制也可以实现类似功能。
虽然有这么多轻量级线程的实现,但几乎都要自己进行调度,使用起来还是有相当难度的。每个“线程”中不能有阻塞操作,否则会使整个机制失效。


更好的方案
erlang提供了相对较好的解决方案。因为erlang在虚拟机上实现了轻量级线程和调度器,并且成为语言的基础。erlang里面用户并不控制线程,而是创建大量的轻量级线程,erlang里面称为进程(process)。每个进程都可代表一个主动对象,它有事件循环,各个进程间通过消息来通讯。一个进程向另一进程发送消息后,可以进入接收状态,这时候真正的线程会把执行权切换到其它进程,如果另一进程得到执行权,如果它回复了消息,再经过一些执行权的切换以后,原来发送消息的进程得到执行权,它就可以收到消息了,这个复杂的过程可能只需要一个真正意义上的线程就可以完成,程序的编写也是使用同步的方式,完全感觉不到底层的切换,你唯一需要的就是毫无顾忌地创建进程。erlang底层通常只需要1个线程就可以完成这些复杂的工作,因为没有一个进程阻塞。在多CPU机器上,可以选择创建和CPU数目相等的进程数来提高效率。
erlang自身是完美的,但它却无法避免使用一些“不完美”的库。比如它要调用MySQL,阻塞查询数据库,总不能把erlang虚拟机完全阻死吧?这个时候可以有2种选择,一种是不使用mysql的API,而是用erlang来实现mysql远程调用协议;另一种是写一个mysql调用程序,它和erlang通过网络或其它方式来通讯,接收查询指令,执行查询,并发回数据,这在erlang里面称为c-node方式。

erlang里面是怎么把这些复杂的异步调用都屏蔽掉的?留给我们的竟然只是简单的同步操作方式?因为它底层就是使用异步机制,用异步操作封装了整个系统,开放给用户的是轻量级线程,完全是同步操作方式(从这里看好像和node.js差不多对不对),用户不需要显式地请求线程切换,erlang虚拟机会以函数为单位进行调度,由于erlang不使用循环语句,代之以递归,所以即使是一个无限递归也不会导致其它进程不能获得执行权,erlang里面进程的调度是软实时的,它保证所有进程都能尽量平等地获得执行时间。

事实上,对于其它语言来说,完全写成异步也是可行的。但是问题在于,在语言层面,程序员来告诉虚拟机“这两个操作同时进行”是很麻烦是事。大多数人懒得去用。

猜你喜欢

转载自blog.csdn.net/FYZDMMCpp/article/details/52317904
今日推荐