多线程服务器的适用场合与常用编程模型
- 进程间通信与线程同步;
- 以最简单规范的方式开发功能正确、线程安全的多线程程序;
- 多线程服务器是指运行在linux操作系统上的独占式网络应用程序;
- 不考虑分布式存储, 只考虑分布式计算;
进程与线程
- 进程(process)是操作系统里最重要的两个概念之一(另一个是文件), 粗略的讲, 一个进程是"内存中正在运行的程序";
- 每个进程有自己独立的地址空间(adress space), "在同一个进程"还是"不在同一个进程"是系统功能划分的重要决策点;
- 把进程比喻成人, 电话谈只能通过周期性的心跳来判断对方是否还活着;
- 容错, 万一有人突然死了;
- 扩容, 新人中途加进来;
- 负载均衡, 把甲的活挪给乙做;
- 退休, 甲要修复bug, 先别派新任务, 等他做完手上的事情就把他重启等等各种场景, 十分便利;
- 线程的特点是共享地址空间, 从而可以高效地共享数据;
- 如果多个进程大量共享内存, 等于是把多进程程序当成多线程来写, 掩耳盗铃;
- "多线程"的价值, 是为了更好地发挥多核处理器(multi-cores)的效能;
- 单核用状态机的思路去写程序是最高效的;
单线程服务器的常用编程模型
- I/O模型, 客户端/服务器设计范式;
- 用得最广的是"non-blocking IO + IO multiplexing"这种模型(非阻塞IO+IO多路复用), 即Reactor模式;
- lighttpd, 单线程服务器(Nginx与之类似, 每个工作进程都有一个eventloop事件循环);
- libevent, libev;
- ACE, Poco C++ libraries;
- Java NIO, 包括Apache Mina 和 Netty;
- POE(Perl);
- Twisted(Python);
- "non-blocking IO + IO multiplexing"这种模型(非阻塞IO+IO多路复用)中, 程序的基本结构是一个事件循环(event loop), 以事件驱动(event-driven)和事件回调的方式实现业务逻辑;
- select/poll有伸缩性方面的不足, Linux下用epoll来进行替换;
- Reactor模型的优点很明显, 编程不难, 效率也不错;
- 不仅可以用于读写socket, 连接的建立(connect/accept)甚至DNS解析都可以用非阻塞方式进行;
- 以提高并发度和吞吐量(throught), 对于IO密集的应用是个不错的选择;
- lighttpd内部的fdevent结构十分精妙, 值得学习;
- 基于事件驱动的编程模型也有其本质的缺点, 它要求事件回调函数必须是非阻塞的;
- 对于涉及网络IO的请求响应式协议, 它容易割裂业务逻辑, 使其散布于多个回调函数中, 相对不容易理解和维护;
多线程服务器的常用编程模型
- 大概有几种:
- 每请求创建一个线程, 使用阻塞式IO操作; 可惜伸缩不佳;
- 使用线程池, 同样使用阻塞式IO操作, 这是提高性能的措施;
- 使用non-blocking IO + IO multiplexing; 即Java NIO的方式;
- Leader/Follower等;
- 默认情况下, 使用non-blocking IO + IO multiplexing模式来编写多线程C++网络服务程序;
- 线程数目基本固定, 可以在程序启动的时候设置, 不会频繁创建与销毁;
- 可以很方便地在线程间调配负载;
- IO事件发生的线程是固定的, 同一个TCP链接不必考虑事件并发;
- Eventloop代表了线程的主循环, 需要让哪个线程干活, 就把timer或IO channel(如TCP连接)注册到哪个线程的loop里即可;
- 多线程程序对event loop提出了更高的要求, 那就是线程安全;
线程池
- 对于没有IO光有计算任务的线程, 使用event loop有点浪费, 一种补充方案是用blocking queue实现的任务队列(TaskQueue);
- BlockingQueue是多线程的利器;
- 无界的BlockingQueue和有界的BoundedBlockingQueue;
- Intel Threading Building Blocks里的concurrent_queue, 性能估计会更好;
- 推荐模式:
- 推荐的C++多线程服务器模式为: one(event) loop per thread + thread pool;
- event loop(也叫IO loop)用作IO multiplexing, 配合non-blocking IO和定时器;
- thread pool用来做计算, 具体可以是任务队列或是生产者消费者队列;
- 写这种方式的服务器程序, 需要一个优质的基于Reactor模式的网络库来支撑, muduo正是这样的网络库;
进程间通信只用TCP
- IPC(进程间通信)主要有: 匿名管道(pipe), 有名管道(FIFO), POSIX消息队列, 共享内存, 信号(signals), 套接字(sockets), 信号量(semaphore);
- 同步原语(synchronization primitives): 互斥量(mutex), 条件变量(condition variable), 读写锁(reader-writer lock), 文件锁(record locking), 信号量(semaphore);
- 贵精不贵多;
- TCP是双向的, Linux的pipe是单向的;
- pipe有一个经典的应用场景, 那就是写Reactor/event loop时用来异步唤醒select(或等价的poll/epoll_wait)调用;
- TCP port由一个进程独占, 且操作系统会自动回收(listening port和已建立连接的TCP socket都是文件描述符, 在进程结束时操作系统会关闭所有文件描述符);
- 快速failover(故障容错), 应用层的心跳也必不可少;
- TCP协议的一个天生的好处是"可记录, 可重现";
- TCP连接是可再生的, 连接的任何一方都可以退出再启动, 重建连接之后就能继续工作, 对开发牢靠的分布式系统意义重大;
- TCP这种字节流(byte stream)方式通信, 会有marshal/unmarshal(编码/解码)的开销;
- 这要求我们选用合适的消息格式(准确地说是wire format-有线格式), 推荐 Google Protocol Buffers;
- TCP的local吞吐量一点都不低;
- TCP是字节流, 只能顺序读取, 有写缓冲;
- 共享内存是消息协议, a进程填好一块内存让b进程来读, 基本是"stop wait(停等)"方式;
- 要将这两种方式揉到一个程序里, 需要建立一个抽象层, 封装两个IPC;
- 生产环境下的数据库服务器往往是独立的高配置服务器, 一般不会同时运行其他占资源的程序;
- TCP是个数据流协议, 除了直接使用它通信外, 还可以在此之上构建RPC/HTTP/SOAP之类的上层通信协议;
- 除点对点的通信之外, 应用级的广播协议也是非常有用的, 可方便地构建可观可控的分布式系统;
分布式系统中使用TCP长连接通信
- 分布式的软件设计和功能划分一般以"进程"为单位;
- 分布式系统采用TCP长连接通信;
- 必要时可以借助多线程来提高性能;
- 对整个分布式系统, 要做到能scale out, 即享受增加机器带来的好处;
- 使用TCP长连接的好处有两点:
- 容易定位分布式系统中的服务器之间的依赖关系;
- netstat -tpna | grep : port能立即列出客户端地址;
- 二是通过接收和发送队列的长度也较容易定位网络或程序故障;
多线程服务器的适用场合
- 开发服务器端程序的一个基本任务是处理并发连接, 现在服务端网络编程处理并发连接主要有两种方式:
- 当线程很廉价时, 一台机器上可以创建远高于CPU数目的线程;
- 当线程很宝贵时, 一台机器上只能创建与CPU数目相当的线程;
- 必须用单线程的场合:
- 一个程序fork之后, 一般有两种行为:
- 立刻执行exec(), 变身为另一个程序(负责启动job的守护进程);
- 不调用exec(), 继续运行当前程序;
- 要么通过共享的文件描述符与父进程通信, 协同完成任务;
- 要么接过父进程传来的文件描述符, 完成独立的任务;
- 只有看门狗线程必须坚持单线程, 其他的均可替代为多线程程序(从功能上讲);
- 单线程程序能限制程序的CPU占用率;
- 单线程程序的优缺点:
- 单线程程序的优势: 简单, 一个基于IO multiplexing的event loop;
- event loop有一个明显的缺点, 它是非抢占式的(non-preemptive), 有点类似优先级反转;
- 这个缺点可以用多线程来克服, 这也是多线程的主要优势;
- 多线程一般没有性能上的优势:
- 用很少的CPU负载就能让IO跑满, 或者用很少的IO流量就能让CPU跑满, 那么多线程就没有啥用处;
- 适用多线程程序的场景:
- 提高响应速度, 让IO和计算互相重叠, 降低latency(延迟);
- 虽然多线程不能提高绝对性能, 但多线程能提高平均响应性能;
- 一个程序要想做多线程, 大致要满足:
- 有多个CPU可用;(单核机器上多线程没有性能优势, 或许能简化并发业务逻辑的实现);
- 线程间有共享数据, 即内存中的全局状态, 如果没有共享数据, 用运行多个单线程的进程就行;
- 提供非均质的服务; 事件的响应有优先级的差异, 用专门的线程来处理优先级高的事件, 防止优先级反转;
- latency和throughout同样重要, 不是逻辑简单的IO bound或CPU bound程序(程序有相当的计算量);
- 利用异步操作;
- 能scale up, 一个好的线程程序能享受增加CPU数目带来的好处;
- 具有可预测的性能, 线程数一般不随负载变化;
- 多线程能有效地划分责任和功能, 让每个线程的逻辑比较简单, 任务单一, 便于编码;
- 线程的分类:
- IO线程, 这类线程的主循环是IO multiplexing, 阻塞地等在select/poll/epoll_wait系统调用上;
- 计算线程, 这类的主循环是blocking queue, 阻塞地等待在condition variable上, 这类线程主要位于thread pool中;
- 第三方库所用的线程, 比如logging, 或database connection;
- 学习多线程编程还有一个好处, 即训练异步思维, 提高分析并发事件的能力;
- 运行在多台机器上的服务进程本质上是异步的;
- 熟悉多线程编程的话, 很容易就能发现分布式系统在消息和事件处理方面的race condition;
多线程服务器的适用场合
- 32位Linux, 一个进程的地址空间是4GB, 其中用户态能访问3GB左右, 而一个线程的默认栈(stack)大小是10MB, 一个进程大概能开300个线程;
- 所谓基于事件指的是用IO multiplexing event loop的编程模型, 又称Reactor模式;
- 单个的event loop处理1万个并发长连接并不稀罕, 一个multi-loop的多线程程序应该能轻松支持5万并发连接;
- thread per connection 不适合高并发场合, 其scalability不佳, one loop per thread 的并发度足够大, 且与CPU数目成正比;
- 多线程能提高吞吐量吗?
- 对于计算密集型服务, 不能;
- 为了在并发请求数很高时也能保持稳定额吞吐量, 我们可以用线程池, 线程池的大小应该满足"阻抗匹配原则";
- 线程池也不是万能的, 如果响应一次请求需要做比较多的计算可以用线程池;
- 如果一次请求响应中, 主要时间是等待IO, 那么为了进一步提高吞吐量, 往往要用其他编程模型, 比如Proactor;
- 多线程能降低响应时间么?
- 如果设计合理, 充分利用多核资源的话, 多线程可以降低响应时间, 在突发(burst)请求时效果尤为明显;
- 多线程程序如何让IO和计算互相重叠, 降低latency(延迟)?
- 把IO操作(通常是写操作)通过BlockingQueue交给别的线程去做, 自己不必等待;
- 为什么第三方库往往要用自己的线程?
- event loop模型没有标准实现;
- libmemcached只支持同步操作;
- MySQL的官方C API不支持异步操作;
- 什么是线程池大小的阻抗匹配原则?
- 线程池的经验公式T=C/P;
- 考虑操作系统能灵活、合理地调度sleeping/writing/running线程;
- 除了你推荐的Reactor+thread pool, 还有别的non-trivial多线程编程模型吗?
- 有, Proactor, 如果一次请求响应中要和别的进程打多次交道, 那么Proactor模型往往能做到更高的并发度;
- Proactor模式依赖操作系统或库来高效地调度这些子任务, 每个子任务都会阻塞, 因此能用比较少的线程达到很高的IO并发度;
- Proactor能提高吞吐, 但不能降低延迟;
- Proactor模式让代码非常破碎, 在C++中使用Proactor是很痛苦的;
- 多线程的进程和多个相同的单线程进程如何取舍?
- 在其他条件相同的情况下, 可以根据工作集(work set)的大小来取舍;
- 工作集是指服务程序响应一次请求访问内存大小;
- 如果工作集较大, 那么就用多线程, 避免CPU cache换入换出, 影响性能;否则, 就用单线程多进程, 享受单线程编程的便利;
- 线程不能减少工作量, 即不能减少CPU时间;
posted @
2018-10-10 22:51
coding-for-self
阅读(
468)
评论()
编辑
收藏
举报