
当在读这篇文章的时候,你有没有想过,服务器是怎么把这篇文章发送给你的呢? 说简单也简单,不就是一个用户请求吗?服务器根据请求从数据库中捞出这篇文章,然后通过网络发回去。 说复杂也复杂,服务器是如何并行处理成千上万个用户请求呢?这里面涉及到哪些技术呢? 这篇文章就来为你解答这个问题。 $ }! t! P" V ^# E+ y# O 多进程 历史上最早出现也是最简单的一种并行处理多个请求的方法就是利用多进程。比如在Linux世界中,我们可以使用fork、exec等系统调用创建多个进程,我们可以在父进程中接收用户的连接请求,然后创建子进程去处理用户请求,就像这样:![]()
多线程 不是创建进程开销大吗?不是进程间通信困难吗?这些对于线程来说统统不是问题。由于线程共享进程地址空间,因此线程间通信天然不需要借助任何通信机制,直接读取内存就好了。线程创建销毁的开销也变小了,要知道线程就像寄居蟹一样,房子(地址空间)都是进程的,自己只是一个租客,因此非常的轻量级,创建销毁的开销也非常小。
![]() ![]() Event Loop:事件驱动 到目前为止,我们提到“并行”二字就会想到进程、线程。但是,并行编程只能依赖这两项技术吗,并不是这样的。还有另一项并行技术广泛应用在GUI编程以及服务器编程中,这就是近几年非常流行的事件驱动编程,event-based concurrency。大家不要觉得这是一项很难懂的技术,实际上事件驱动编程原理上非常简单。这一技术需要两种原料:
![]()
![]() 事件来源:IO多路复用 我们知道,在Linux/Unix世界中一切皆文件,而我们的程序都是通过文件描述符来进行I/O操作的,当然对于socket也不例外,那我们该如何同时处理多个文件描述符呢?IO多路复用技术正是用来解决这一问题的,通过IO多路复用技术,我们一次可以监控多个文件描述,当某个文件(socket)可读或者可写的时候我们就能得到通知啦。这样IO多路复用技术就成了event loop的原材料供应商,源源不断的给我们提供各种event,这样关于event来源的问题就解决了。
![]() 想一想还有没有其它问题?6 a% {- L9 f3 H# q) n* F; _ 问题:阻塞式IO 现在,我们可以使用一个线程(进程)就能基于事件驱动进行并行编程,再也没有了多线程中让人恼火的各种锁、同步互斥、死锁等问题了。但是,计算机科学中从来没有出现过一种能解决所有问题的技术,现在没有,在可预期的将来也不会有。那上述方法有什么问题吗?不要忘了,我们event loop是运行在一个线程(进程),这虽然解决了多线程问题,但是如果在处理某个event时需要进行IO操作会怎么样呢?程序员最常用的这种IO方式被称为阻塞式IO,也就是说,当我们进行IO操作,比如读取文件时,如果文件没有读取完成,那么我们的程序(线程)会被阻塞而暂停执行,这在多线程中不是问题,因为操作系统还可以调度其它线程。但是在单线程的event loop中是有问题的,原因就在于当我们在event loop中执行阻塞式IO操作时整个线程(event loop)会被暂停运行,这时操作系统将没有其它线程可以调度,因为系统中只有一个event loop在处理用户请求,这样当event loop线程被阻塞暂停运行时所有用户请求都没有办法被处理,你能想象当服务器在处理其它用户请求读取数据库导致你的请求被暂停吗?
![]() 有的同学可能会问,如果不能发起阻塞式IO的话,那么该怎样进行IO操作呢?有阻塞式IO,就有非阻塞式IO。1 m/ F, F" R3 g) r2 |5 M 非阻塞IO 为克服阻塞式IO所带来的问题,现代操作系统开始提供一种新的发起IO请求的方法,这种方法就是异步IO,对应的,阻塞式IO就是同步IO。异步IO时,假设调用aio_read函数(具体的异步IO API请参考具体的操作系统平台),也就是异步读取,当我们调用该函数后可以立即返回,并继续其它事情,虽然此时该文件可能还没有被读取,这样就不会阻塞调用线程了。此外,操作系统还会提供其它方法供调用线程来检测IO操作是否完成。就这样,在操作系统的帮助下IO的阻塞调用问题也解决了。+ k# [# q' c ^$ l8 N, ~2 r* A基于事件编程的难点 虽然有异步IO来解决event loop可能被阻塞的问题,但是基于事件编程依然是困难的。首先,我们提到,event loop是运行在一个线程中的,显然一个线程是没有办法充分利用多核资源的,有的同学可能会说那就创建多个event loop实例不就可以了,这样就有多个event loop线程了,但是这样一来多线程问题又会出现。另一点在于编程方面,异步编程需要结合回调函数,这种编程方式需要把处理逻辑分为两部分,一部分调用方自己处理,另一部分在回调函数中处理,这一编程方式的改变加重了程序员在理解上的负担,基于事件编程的项目后期会很难扩展以及维护。那么有没有更好的方法呢?要找到更好的方法,我们需要解决问题的本质,那么这个本质问题是什么呢?0 N _ p( H4 S更好的方法 为什么我们要使用异步这种难以理解的方式编程呢?是因为阻塞式编程虽然容易理解但会导致线程被阻塞而暂停运行。那么聪明的你一定会问了,有没有一种方法既能结合同步IO的简单理解又不会因同步调用导致线程被阻塞呢?答案是肯定的,这就是用户态线程,user level thread,也就是大名鼎鼎的协程,关于协程值得单独拿出一篇文章来讲解,就在下一篇。虽然基于事件编程有这样那样的缺点,但是在当今的高性能高并发服务器上基于事件编程方式依然非常流行,但已经不是纯粹的基于单一线程的事件驱动了,而是event loop + multi thread + user level thread。3 |5 X9 N. }8 x$ D* s8 v2 N( B总结 高并发技术从最开始的多进程一路演进到当前的事件驱动,计算机技术就像生物一样也在不断演变进化,但不管怎样,了解历史才能更深刻的理解当下。希望这篇文章能对大家理解高并发服务器有所帮助。 |