大规模并发服务的技术,归纳起来就是两种方式:
-
thread per client,用blocking I/O
-
多个clients一个thread,用nonblocking I/O或asynchronous I/O
Linux asynchronous I/O还不好,一般都是用nonblocking I/O
多数都是用epoll()的edge triggering(select()性能问题)
处理并发方式
引出thread和event之争
实际系统当中往往是混合系统
事件驱动处理网络事件,线程处理事务
os(尤其是Linux)和语言的限制(Java/C/C++等),线程无法实现大规模的并发事务
一般机器要保证性能的话,线程数量基本要限制在几百(Linux线程达到一定数量以后,会导致系统性能指数下降,SEDA的论文)
高性能web server都用事件驱动机制,nginx,Tornado,node.js等
线程和事件,或者说同步和异步之争学术领域争了几十年
1978年论文证明用线性的process(线程的模式)和消息传递(事件的模式)是等价的,而且如果实现合适,两者应该有同等性能
针对事件驱动的流行,2003年伯克利论文“Why events are a bad idea (for high-concurrency servers)”,指出其实事件驱动并没有在功能上有比线程有什么优越之处,但编程要麻烦很多,而且特别容易出错
线程的问题
无非是目前的实现的原因
- 占资源太大,一创建就分配几个MB的stack,一般机器能支持的线程大受限制
可用自动扩展的stack,创建时先少分点,然后动态增加
- 线程切换成本大,Linux中实际上process和thread是一回事,区别就在于是否共享地址空间
可用轻量级的线程实现,通过合作式的办法实现共享系统线程
用coroutine和nonblocking I/O(用的是poll()+thread pool)实现了一个原型系统,证明了性能并不比事件驱动差
是不是线程只要实现的好就行了?
2006年还是伯克利论文“The problem with threads”线程也不行
目前程序模型基本上是基于顺序执行(确定性,容易保证正确性),人的思维方式也往往是单线程的
线程的模式是强行在单线程,顺序执行的基础上加入了并发和不确定性
这样程序的正确性就很难保证
线程间同步通过共享内存实现,很难来对并发线程和共享内存来建立数学模型,其中有很大的不确定性,而不确定性是编程的巨大敌人
项目经验说明保证多线程的程序的正确性,几乎是不可能的事情
很多很简单的模式,在多线程的情况下,要保证正确性,需要注意很多非常微妙的细节,否则就会导致deadlock或者race condition
人的思维的限制,即使采取各种消除不确定的办法
monitor,transactional memory,promise/future等机制,还是很难保证面面俱到
作者有计算机科学的专家,有最聪明的研究生,采用了整套软件工程的流程
- design review
- code review
- regression tests
- automated code coverage metrics
认为已经消除了大多数问题,不过还是在系统运行4年以后,出现了一个deadlock
作者说很多多线程的程序实际上存在并发错误,只不过由于硬件的并行度不够,往往不显示出来
随着硬件的并行度越来越高,很多原来运行完好的程序,很可能会发生问题
程序NPE,core dump都不怕,最怕的就是race condition和deadlock,因为这些都是不确定的(non-deterministic),往往很难重现
线程+共享内存不行
研究领域一些模型开始被新的程序语言采用
Actor模型
用一些并发的实体(actor),之间的通过发送消息来同步
所谓“Don’t communicate by sharing memory, share memory by communicating”
Actor模型和线程的共享内存机制是等价的
实际上,Actor模型一般通过底层的thread/lock/buffer 等机制来实现,是高层的机制
Actor模型是数学上的模型,有理论的支持
另一个类似的数学模型是CSP(communicating sequential process)
早期的实现这些理论的语言最著名的就是erlang和occam
尤其是erlang,所谓的Ericsson Language,目的就是实现大规模的并发程序,用于电信系统
Go的并发实体goroutine,类似coroutine,但不需要自己调度。Runtime把goroutine调度到系统的线程运行,多个goroutine共享一个线程
如果有一个操作要阻塞,Runtime把属于此线程执行的其他的goroutine调度到其他的线程上去