进程与线程
进程是操作系统里最重要的两个概念之一(另一个是文件)。粗略为内存中正常运行的程序,在 linux 端是通过 fork() 系统调用出来的东西,windows 下用 CreateProcess() 的产物。
进程独立的地址空间,线程共享地址空间。
单线程服务器常用编程模型
non-blocking IO + IO multiplexing 即 Reactor 模型。如下框架:
- lighttpd,单线程服务器。(与 Nginx 类似,每个工作进程又一个 event loop)
- libevent,libev。
- ACE,Poo C++ libraries。
- Java NIO,包括 Apache Mina 和 Netty
- POE (Perl)
- Twisted (Python)
Boost.Asio 和 Windows I/O Completion Ports 实现了 Proactor 模型。ACE 也实现了Proactor 模型。
多线程服务器常用编程模型
- 每个请求创建一个线程,使用阻塞 IO 操作。可伸缩性不佳。
- 使用线程池,同样使用阻塞 IO 操作,与 1 比,提高性能的措施。
- 使用 non-blocking-IO + IO multiplexing 。即 Java NIO 方式
- Leader/Follower 等高级模式
one loop per thread
程序中每个 IO 线程有一个 event loop (或叫 Reactor)。 用于处理读写或定时事件。
好处:
- 线程数据基本固定,可在程序启动时设置,不会频繁创建和销毁。
- 可以方便的在线程间调配负载。
- IO 事件发生的线程是固定的,同一个 TCP 连接不必考虑事件并发
线程池
muduo 中 ThreadPool,BlockingQueue, BoundedBlockingQueue
BlockingQueue
Intel Threading Building Blocks 里的 concurrent_queue
推荐模式
one loop per thread + threadpool
- event loop (也叫 IO loop) 用作 IO multiplexing 。 配合 non-blocking IO 和定时器
- thread pool 用来作计算,具体可以是任务队列或生产者消费者队列
进程间通信只用 TCP
进程间通信首先 Sockets ,可跨主机,具有伸缩性。
tcpdump + WireShark 分析网络利器,可编写自动化回归测试,也可用 tcpcopy 之类工具进行压力测试
数据格式推荐 Google Protocol Buffers
TCP 长连接好处:
- 容易定位分布式系统中的服务之间的依赖关系。netstat -tpna | grep :port 可以找到服务的客户端地址。在客户端使用 netstat 或 lsof 查看哪个进程发起的连接。
- 通过接收和发送队列长度也容易定位网络或程序故障。netstat 的 Recv-Q 或 Send-Q 正常在 0 上下波动或为 0,如果 Recv-Q 不变或增加可能发生死锁或阻塞。如果 Send-Q 不变或增加,可能服务器太忙,来不及处理,也可能中间网络路由器故障,丢包,甚至对方服务器掉线。
多线程服务器的适用场合
服务端网络处理并发连接两种方式:
- 当“线程”很廉价时,一台机器上可以创建远高于 CPU 数目的“线程”。这时一个线程只处理一个 TCP 连接(甚至半个),通常使用阻塞 IO (至少看起来如此)。例如:Python gevent、Go goroutine、Erlang actor。这里的“线程”由语言的 runtime 自行调度,与操作系统线程不是一回事。
- 当线程很宝贵时,一台机器上只能创建与 CPU 数目相当的线程。这时一个线程要处理多个 TCP 连接上的 IO,通常使用非阻塞 IO 和 IO multiplexing。例如:libevent、muduo、Netty。这是原生线程,能被操作系统的任务调度看见。
model:
- 运行一个单线程的进程
- 运行一个多线程的进程
- 运行多个单线程的进程
- 运行多个多线程的进程
- 模式1是不可伸缩的,不能发挥多核的能力。
- 模式3是目前公认的主流模式。有以下两种子模式:
- 简单的把模式1的进程运行多份
- 主进程 +worker 进程,如果必须绑定到一个 TCP port ,比如 httpd + fastcgi
必须用单线程的场合
- 程序可能会 fork(2)
- 限制程序的 CPU 占有率
一个 fork(2) 之后一般有两种行为:
- 立刻执行 exec(), 变身为另一个程序。例如:shell 和 inetd。又比如 lighttpd fork() 出子进程,然后运行 fastcgi 程序。或者集群中运行再计算节点上的负责启动 job 的守护进程(即“看门狗进程")
- 不调用 exec(), 继续执行当前程序。
以上只有看门狗进程必须坚持单线程,其他均可替换为多线程程序(功能上)。
单线程程序的优缺点
优势:简单。
Event loop 明显缺点,非抢占。
多线程有优势吗?
- IO bound 或 CPU bound 服务没有优势。用很少的 CPU 负载就能让 IO 跑满或用很少的 IO 流量就能让 CPU跑满,多线程没啥用。
- 对于静态 web 页面,或者 ftp 服务器。cpu 负载很轻,主要瓶颈在磁盘 io 和网络 io 方面。这个使用用模式1 一个单线程程序就能撑满 io。用多线程并不能提高吞吐量,因为硬件容量已经饱和。同理增加 CPU 也不能提高吞吐量。
适用多线程程序的场景
提高响应速度,让 IO 和 “计算”相互重叠,降低 latency。虽然多线不能提高绝对性能,但是能提高平均响应性能。
一个程序做成多线程,大致满足:
- 有多个 cpu 可用。
- 线程间有共享数据,即内存中全局状态。如果没有共享数据用模式中主进程 +worker 进程。
- 共享数据是可修改的,而不是静态的常量表。如果数据不能修改可以用进程间 shared memory。模式3即可。
- 提供非均质的服务。即事件的响应有优先级差异,用专门的线程处理优先级高的事件。防止优先级反转。
- latency 和 throughput 同样重要。不是简单的 io bound 和 cpu bound。程序有相当的计算量。
- 利用异步操作。比如 logging,无法 log file 还是 log server,都不应该阻塞 critical path(关键路径)
- 能 scale up。
- 具有可预测的性能。随负载增加,性能缓慢下降,超过一个值后会急速下降。线程数目一般不随负载变化。
- 多线程能有效的划分责任和功能,让每个线程的逻辑比较简单,任务单一,便于编码。而不是所有的逻辑塞到一个 event loop 里,不同类别的事件之间相互影响。
线程分类
- IO 线程。这类线程主循环的 IO multiplexing。阻塞的等待在 select/poll/epoll_wait 系统调用上。功能不止 IO,简单的计算也可以放入其中,比如消息的编码和解码
- 计算线程。这类线程主要循环是 blocking queue,阻塞的等待在 condition variable上。这类线程一般位于 thread pool 中。不涉及 IO,一般避免任何阻塞操作。
- 第三方库所用的线程。比如 logging。又比如 database connection
Linux 能启动多少个线程
对于 32bit linux,一个进程地址空间 4G,其用户态能访问 3GB 左右,一个线程默认栈大小10MB,一个继承大约启动 300 多个线程。如果不改调用栈大小,300左右是上限,程序的其他部分(数据段、代码段、堆、动态库等)同样要占用内存。
对于 64bit linux 线程数目大大增加,:64 位系统的虚拟地址空间理论上为 2⁶⁴ 字节(远超物理内存),但线程栈的虚拟地址分配会占用连续地址空间,极端情况下可能因地址碎片导致无法分配新栈(即使内存充足)。 不考虑内核对线程限制。(16GB 内存):若线程栈设为默认 8MB,且其他资源充足,实际可启动约 1000~2000 个线程(受限于物理内存)。
多线程能提高并发度吗
并发连接数 不能
thread per connection 不适合高并发场合,其 scalability 不佳。one loop per thread 的并发度足够大,其与 cpu 数目成正比。
| 维度 | thread-per-connection | one-loop-per-thread |
|---|---|---|
| 线程数与连接数关系 | 1:1(连接多则线程多) | 1:N(一个线程处理多个连接) |
| 线程数与 CPU 关系 | 无关(可能远多于 CPU 核心) | 成正比(通常等于 CPU 核心数) |
| 资源开销 | 高(线程栈 + 调度成本) | 低(固定线程 + 事件驱动) |
| 并发上限 | 低(受限于线程数和内存) | 高(受限于 IO 多路复用能力) |
| 适用场景 | 低并发、短连接(如内部服务) | 高并发、长连接(如 Web 服务器) |
多线程能提高吞吐量吗
对于计算密集型服务,不能
多线程能降低响应时间吗
设计合理,充分利用多核 可以
多线程程序如何让 IO 和 “计算” 相互重叠,降低 latency
基本思路:把 IO 操作(通常是写操作)通过 BlockingQueue 交给别的线程去做,自己不必等待。
为什么第三方库往往用自己的线程
第三方库不一定能很好的适用并融入这个 event loop framework,有时需要用线程来做一些的串并转换。
什么是线程池大小的阻抗匹配原则
T 线程数 * P 每个线程占用 cpu 的时间 = C 占用 cpu 的个数
C = 8 T = 1.0 那么 T = 8 正好跑满 cpu
C = 8 T = 0.5 那么 T = 16 跑满 cpu 50% 的线程能让 cpu 繁忙,启动再多只会增加上下文的切换开销而降低性能,并不能再提高吞吐量
除了 Ractor 模型,其他的 non-trivial 模型
Proactor,如果一次请求响应中要和别的进程打多次交道,那么 Proactor 模型能做到更高的并发度。代价是,代码变得支离破碎,难以理解。
一个多线程得进程和多个相同得单线程进程的模型如何选择
工作集指服务程序响应一次请求访问的内存大小。
根据工作集做取舍,访问内存大,则用多线程,可以进程内共享数据。访问内存小,可以用单线程的多个进程,享受编程的简单性。
Reference
《Erlang 程序设计》
http://www.cs.wustl.edu/~schmidt/PDF/proactor.pdf
http://www.cs.wustl.edu/~schmidt/PDF/Reactor1-93.pdf,Reactor2-93.pdf,Reactor.pdf
http://www.kegel.com/c10k.html
http://www.cs.uwaterloo.ca/~brecht/pubs.html
http://hal.inria.fr/docs/00/67/44/75/PDF/paper.pdf
http://pod.tst.eu/http://cvs.schmorp.de/libdev/ev.pod#THREADS_AND_COROUTINES
http://blog.csdn.net/haoel/article/details/2224055
http://code.google.com/p/tcpcopy/
http://kernel.org/pub/linux/kernel/people/paulmck/perfbook/perfbook.html
http://blog.codingnow.com/2006/04/iocp_kqueue_epoll.html
https://computing.llnl.gov/linux/slurm
http://www.slideshare.net/chenshuo/zurg-part-1
http://blog.csdn.net/Solstice/article/details/5334243
http://www.cs.berkeley.edu/~culler/papers/events.pdf
http://blog.csdn.net/Solstice/article/details/2096209