zoukankan      html  css  js  c++  java
  • Mudo C++网络库第三章学习笔记

    多线程服务器的适用场合与常用编程模型

    • 进程间通信与线程同步;
    • 以最简单规范的方式开发功能正确、线程安全的多线程程序;
    • 多线程服务器是指运行在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提出了更高的要求, 那就是线程安全;
      • 多线程的Reactor;

    线程池

    • 对于没有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;
      • 限制程序的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时间;
  • 相关阅读:
    UVA12125 March of the Penguins (最大流+拆点)
    UVA 1317 Concert Hall Scheduling(最小费用最大流)
    UVA10249 The Grand Dinner(最大流)
    UVA1349 Optimal Bus Route Design(KM最佳完美匹配)
    UVA1212 Duopoly(最大流最小割)
    UVA1395 Slim Span(kruskal)
    UVA1045 The Great Wall Game(二分图最佳匹配)
    UVA12168 Cat vs. Dog( 二分图最大独立集)
    hdu3488Tour(KM最佳完美匹配)
    UVA1345 Jamie's Contact Groups(最大流+二分)
  • 原文地址:https://www.cnblogs.com/longjiang-uestc/p/9769813.html
Copyright © 2011-2022 走看看