当在读这篇文章的时候,你想过没有,服务器是怎么把这篇文章发送给你的呢?
说简略也简略,不就是一个用户申请吗?服务器依据申请从数据库中捞出这篇文章,而后通过网络发回去。
说简单也简单,服务器是如何 并行处理成千上万个用户申请呢?这外面波及到哪些技术呢?
这篇文章就来为你解答这个问题。
多过程
历史上最早呈现也是最简略的一种并处解决多个申请的办法就是利用 多过程。
比方在 Linux 世界中,咱们能够应用 fork、exec 等办法创立多个过程,咱们能够在父过程中接管用户的链接申请,而后创立子过程去解决用户申请,就像这样:
这种办法的长处就在于:
- 编程简略,非常容易了解
- 因为各个过程的地址空间是互相隔离的,因而一个过程解体后并不会影响其它过程
- 充分利用多核资源
多过程并行处理的长处和显著,然而毛病同样显著:
- 各个过程地址空间互相隔离,这一长处也会变成毛病,那就是过程间要想通信就会变得比拟艰难,你须要借助过程间通信 (IPC,interprocess communications) 机制,想一想你当初晓得哪些过程间通信机制,而后让你用代码实现呢?显然,过程间通信编程绝对简单,而且性能也是一大问题
- 咱们晓得创立过程开销是比线程要大的,频繁的创立销毁过程无疑会减轻零碎累赘。
幸好,除了过程,咱们还有线程。
多线程
不是创立过程开销大吗?不是过程间通信艰难吗?这些对于线程来说通通不是问题。
什么?你还不理解线程,连忙看看这篇《看完这篇还不懂高并发中的线程与线程池你来打我(内含 20 张图)》,这里具体解说了线程这个概念是怎么来的。
因为线程共享过程地址空间,因而线程间通信人造不须要借助任何通信机制,间接读取内存就好了。
线程创立销毁的开销也变小了,要晓得线程就像寄居蟹一样,房子 (地址空间) 都是过程的,本人只是一个租客,因而十分的 轻量级,创立销毁的开销也十分小。
咱们能够为每个申请创立一个线程,即便一个线程因执行 I / O 操作——比方读取数据库等——被阻塞暂停运行也不会影响到其它线程,就像这样:
但线程就是完满的、包治百病的吗,显然,计算机世界素来没有那么简略。
因为线程共享过程地址空间,这在为线程间通信带来便当的同时也带来了无尽的麻烦。
正是因为线程间共享地址空间,因而一个线程解体会导致整个过程解体退出,同时线程间通信几乎太简略了,简略到线程间通信只须要间接读取内存就能够了,也简略到呈现问题也极其容易,死锁、线程间的同步互斥、等等,这些极容易产生 bug,有数程序员贵重的工夫就有相当一部分用来解决多线程带来的无尽问题。
尽管线程也有毛病,然而相比多过程来说,线程更有劣势,但想单纯的利用多线程就能解决高并发问题也是不切实际的。
因为尽管线程创立开销相比过程小,但仍然也是有开销的,对于动辄数万数十万的链接的高并发服务器来说,创立数万个线程会有性能问题,这包含内存占用、线程间切换,也就是调度的开销。
因而,咱们须要进一步思考。
Event Loop:事件驱动
到目前为止,咱们提到“并行”二字就会想到过程、线程。然而,并行编程只能依赖这两项技术吗,并不是这样的。
还有另一项并行技术广泛应用在 GUI 编程以及服务器编程中,这就是近几年十分风行的事件驱动编程,event-based concurrency。
大家不要感觉这是一项很难懂的技术,实际上事件驱动编程原理上非常简单。
这一技术须要两种原料:
- event
- 解决 event 的函数,这一函数通常被称为 event handler
剩下的就简略了:
你只须要宁静的期待 event 到来就好,当 event 到来之后,检查一下 event 的类型,并依据该类型找到对应的 event 处理函数,也就是 event handler,而后间接调用该 event handler 就好了。
That’s it !
以上就是事件驱动编程的全部内容,是不是很简略!
从下面的探讨能够看到,咱们须要一直的接管 event 而后解决 event,因而咱们须要一个循环(用 while 或者 for 循环都能够),这个循环被称为 Event loop。
应用伪代码示意就是这样:
while(true) {event = getEvent();
handler(event);
}
Event loop 中要做的事件其实是非常简单的,只须要期待 event 的带来,而后调用相应的 event 处理函数即可。
留神,这段代码只须要运行在一个线程或者过程中,只须要这一个 event loop 就能够同时解决多个用户申请。
有的同学能够仍然不明确为什么这样一个 event loop 能够同时解决多个申请呢?
起因很简略,对于 web 服务器来说,解决一个用户申请时大部分工夫其实都用在了 I / O 操作上,像数据库读写、文件读写、网络读写等。当一个申请到来,简略解决之后可能就须要查询数据库等 I / O 操作,咱们晓得 I / O 是十分慢的,当发动 I / O 后 咱们大能够不必期待该 I / O 操作实现就能够持续解决接下来的用户申请。
当初你应该明确了吧,尽管上一个用户申请还没有解决完咱们其实就能够解决下一个用户申请了,这就是并行,这种并行就能够用事件驱动编程来解决。
这就好比餐厅服务员一样,一个服务员不可能始终等这上一个顾客下单、上菜、吃饭、买单之后才接待下一个顾客,服务员是怎么做的呢?当一个顾客下完单后间接解决下一个顾客,当顾客吃完饭后会本人回来买单结账的。
看到了吧,同样是一个服务员也能够同时解决多个顾客,这个服务员就相当于这里的 Event loop,即便这个 event loop 只运行在一个线程 (过程) 中也能够同时解决多个用户申请。
置信你曾经对事件驱动编程有一个清晰的认知了,那么接下来的问题就是事件驱动、事件驱动,那么这个事件也就是 event 该怎么获取呢?
事件起源:IO 多路复用
从《终于明确了,一文彻底了解 I / O 多路复用》这篇文章中咱们晓得,在 Linux/Unix 世界中所有皆文件,而咱们的程序都是通过文件描述符来进行 I / O 操作的,当然对于 socket 也不例外,那咱们该如何同时解决多个文件描述符呢?
IO 多路复用技术正是用来解决这一问题的,通过 IO 多路复用技术,咱们一次能够监控多个文件形容,当某个文件 (socket) 可读或者可写的时候咱们就能失去告诉啦。
这样 IO 多路复用技术就成了 event loop 的发动机,源源不断的给咱们提供各种 event,这样对于 event 起源就解决了。
当然对于 IO 多路复用技术的具体解说请参见《终于明确了,一文彻底了解 I / O 多路复用》。
至此,对于利用事件驱动来实现并发编程的所有问题都解决了吗?event 的起源问题解决了,当失去 event 后调用相应的 handler,看上去功败垂成了。
想一想还有没有其它问题?
问题:阻塞式 IO
当初,咱们能够应用一个线程 (过程) 就能基于事件驱动进行并行编程,再也没有了多线程中让人恼火的各种锁、同步互斥、死锁等问题了。
然而,计算机科学中素来没有呈现过一种能解决所有问题的技术,当初没有,在可预期的未来也不会有。
那上述办法有什么问题吗?
不要忘了,咱们 event loop 是运行在一个线程(过程),这尽管解决了多线程问题,然而如果在解决某个 event 时须要进行 IO 操作会怎么样呢?
在《读取文件时,程序经验了什么》一文中,咱们解说了最罕用的文件读取在底层是如何实现的,程序员最罕用的这种 IO 形式被称为阻塞式 IO,也就是说,当咱们进行 IO 操作,比方读取文件时,如果文件没有读取实现,那么咱们的程序 (线程) 会被阻塞而暂停执行,这在多线程中不是问题,因为操作系统还能够调度其它线程。
然而在单线程的 event loop 中是有问题的,起因就在于当咱们在 event loop 中执行阻塞式 IO 操作时整个线程 (event loop) 会被暂停运行,这时操作系统将没有其它线程能够调用,因为零碎中只有一个 event loop 在解决用户申请,这样当 event loop 线程被阻塞暂停运行时所有用户申请都没有方法被解决,你能设想当服务器在解决其它用户申请读取数据库导致你的申请被暂停吗?
因而,在基于事件驱动编程时有一条注意事项,那就是不容许发动阻塞式 IO。
有的同学可能会问,如果不能发动阻塞式 IO 的话,那么该怎么进行 IO 操作呢?
有阻塞式 IO,就有非阻塞式 IO。
非阻塞 IO
为克服阻塞式 IO 所带来的问题,古代操作系统开始提供一种新的发动 IO 申请的办法,这种办法就是异步 IO,对于的,阻塞式 IO 就是同步 IO,对于同步和异步这两个概念关注公众号“码农的荒岛求生”并回复数字“5”你就晓得答案了。
异步 IO 时,假如调用 aio_read 函数 (具体的异步 IO API 请参考具体的操作系统平台),也就是异步读取,当咱们调用该函数后能够 立刻返回,并持续其它事件,尽管此时该文件可能还没有被读取,这样就不会阻塞调用线程了。此外,操作系统还会提供其它办法供调用线程来检测 IO 操作是否实现。
就这样,在操作系统的帮忙下 IO 的阻塞调用问题也解决了。
基于事件编程的难点
尽管有异步 IO 来解决 event loop 可能被阻塞的问题,然而基于事件编程仍然是艰难的。
首先,咱们提到,event loop 是运行在一个线程中的,显然一个线程是没有方法充分利用多核资源的,有的同学可能会说那就创立多个 event loop 实例不就能够了,这样就有多个 event loop 线程了,然而这样一来多线程问题又会呈现。
另一点在于编程方面,在《从小白到高手,你须要了解同步与异步》这篇文章中咱们讲到过,异步编程须要联合回调函数,这种编程形式须要把解决逻辑分为两局部,一部分调用方本人解决,另一部分在回调函数中解决,这一编程形式的扭转减轻了程序员在了解上的累赘,基于事件编程的我的项目前期会很难扩大以及保护。
那么有没有更好的办法呢?
要找到更好的办法,咱们须要解决问题的实质,那么这个实质问题是什么呢?
更好的办法
为什么咱们要应用异步这种难以了解的形式编程呢?
是因为阻塞式编程尽管容易了解但会导致线程被阻塞而暂停运行。
那么聪慧的你肯定会问了,有没有一种办法既能联合同步 IO 的简略了解又不会因同步调用导致线程被阻塞呢?
答案是必定的,关注公众号“码农的荒岛求生”,并回复“高性能”你就晓得答案啦。
尽管基于事件编程有这样那样的毛病,然而在当今的高性能高并发服务器上基于事件编程形式仍然十分风行,但曾经不是纯正的基于繁多线程的事件驱动了,而是 event loop + multi thread + user level thread。
对于这一组合,同样值得拿出一篇文章来解说,咱们将在后续文章中具体探讨。
总结
高并发技术从最开始的多过程一路演进到以后的事件驱动,计算机技术就像生物一样也在一直演变进化,但不管怎样,理解历史能力更粗浅的了解当下。心愿这篇文章能对大家了解高并发服务器有所帮忙。