在传统的服务器编程模型中,我们会为每一个请求分配一个线程,请求结束后终止该线程,或者把线程放回线程池。 Java 的 Servlet 就属于这种模型的典型。对于 PHP, Ruby, Python 这些语言,要么对于多线程支持不好,要么在多线程下表现不好,通常我们会为每个请求分配一个进程,但整体模型都差不多。
通常我们的请求并不是一直在消耗CPU, 一个请求的大部分时间都花在等待磁盘,数据库,cache,其他服务返回上面,或者说等待一个事件发生(比如 web 版的 IM, 通常会建立一个 HTTP 请求,等待其他人给你发消息)。 所以对于一个8核的 CPU,支撑100个并发,CPU消耗并不大,负载也不会太高,甚至 1000 左右的并发请求数也不会有大问题。但在高并发下,首先是由于线程数/进程数过多消耗了太多的内存资源,同时大部分CPU都消耗在上下文切换上边,所以系统负载很高,服务器效率大幅度降低,服务响应变慢甚至不可用。
为了让服务器支持高并发,通常我们需要一个新的框架来处理这些请求,比如 Java 这边就有 comet和netty, python 有 tornado, ruby 有 eventmachine。还有很多直接基于 epoll 系统调用, 或者基于 libevent, libev, libcoro的应用。
在解决这个问题时,发展出了 reactor[1] 和 proactor[2] 两种设计模式, 关于 reactor 模式,可以参考一下 libevent, netty 的编程范例。关于 proactor 模式,可以参考一下 node.js, Java NIO.2[3] 的编程范例,在此不做解释。对于 libcoro, jscex[4],从实现层面上仍然可以认为是 proactor 模式,但避免了写大量的 callback 方法,所以更加易用。
go 语言用于解决高并发的技术被成为 goroutine[5], 与 erlang 的 process 类似,但仍然有如下的不同
- erlang 接收消息的主题是 erlang process, go 语言是 channel
- 一个 goroutine 可以引用多个 channel, 同样一个 channel 也可以被多个 goroutine 引用 (对于常规的生产者-消费者模型, goroutine 比 erlang 方便)
- channel 属于强类型,在编译期可以检查出更多的错误,如果一个 goroutine 希望接收多种类型的消息,一般可以引用多个channel
- channel 可以设置为堵塞型或者设定一个 buffer size, 并且可以用select测试 channel 是否已满, erlang 则无法测试(?)
[1] http://en.wikipedia.org/wiki/Reactor_pattern
[2] http://en.wikipedia.org/wiki/Proactor_pattern
[3] http://jcp.org/en/jsr/detail?id=203