大规模并发服务的技术,归纳起来就是两种方式:

  • 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)”,指出其实事件驱动并没有在功能上有比线程有什么优越之处,但编程要麻烦很多,而且特别容易出错

线程的问题

无非是目前的实现的原因

  1. 占资源太大,一创建就分配几个MB的stack,一般机器能支持的线程大受限制

可用自动扩展的stack,创建时先少分点,然后动态增加

  1. 线程切换成本大,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调度到其他的线程上去