文章目录
索引
【Linux多线程服务端编程】| 【01】线程安全的对象生命期管理笔记
【Linux多线程服务端编程】| 【02】线程同步精要
【Linux多线程服务端编程】| 【03】多线程服务器的适用场合和常用编程模型
【Linux多线程服务端编程】| 【04】C++多线程系统编程精要
1 进程与线程
Linux系统编程 | 【03】进程、环境变量、IPC
Linux | Linux中的线程、互斥量、信号量的基本使用
面谈
(同一个服务器)通信,可时刻
知道对方是否已死,电话谈
(不同一个服务器通信)通过心跳包
周期来判断对方是否存活;
【设计分布式需要考虑】:
容错
:突然有服务器宕机;扩容
:增加新发服务器;负载均衡
:将一个服务器的工作移到另外一台;退休
:一个服务器要修复bug,等它处理完任务就重启;
2 单线程服务器的常用编程模型
【Reactor模型】 在高性能网络程序中,最广泛non-blocking IO + IO multiplexing
,适用该模型的有;
- lighttpd,单线程服务器;
- libevent,libev;
- ACE,Poco C++ libraries;
- Java NIO,包括Apache Mina和Netty;
- POE;
- Twisted;
【Reactor模型】 另外一种模型,常见的有:
- Boost.Asio;
- Windows I/O Completion Ports;
2.1 Reactor模型
【基本结构】:事件循环、事件驱动、事件回调(函数必须为非阻塞)方式来实现;
【优点】:效率高,易编写
,可用于读写socket、连接建立、DNS解析等都可用非阻塞
来提高并发,对于IO密集
不错;
3 多线程服务器的常用编程模型
【常见】:
- 每个
请求
创建一个线程,适用阻塞IO
,伸缩性不佳; - 适用
线程池
,适用阻塞IO
操作,比第一种性能略提升; - 适用
non-blocking IO+IO multiplexing
,默认使用这种; - Leader/Follower等高级模型;
默认选择第三种,即non-blocking IO + one loop per thread
来编写;
3.1 one loop per thread
该模型下,每个IO线程
都有一个event loop
即Reactor
,用于处理读写
和定时
事件;
【优点】:
-
线程数目自设置基本
固定
, 不会频繁创建与销毁
; -
可方便再线程间
调配负载
; -
IO事件发生的线程是固定的,同一个TCP连接不必考虑事件并发;
-
Eventloop为线程的
主循环
,若要指定线程干活,即将timer
或IOchannel
注册到哪个线程的loop; -
对
实时性
有要求、数据量大
的connection可单用一个线程; -
将
数据处理任务分摊
到另外几个线程中; -
其他次要的
辅助性connections可共享
一个线程; -
对于
non-trivial服务端
程序,一般会采用non-blockingIO + IOmultiplexing
,每个connect/accept
都会注册到某个event loop
上,每个线程至多只有一个event loop; -
需要线程安全
,允许一个线程给别的线程里塞东西;
3.2 线程池
适用阻塞队列
针对于没有IO只有计算任
务的线程;
【简单实现】:
可参考muduo中的无界BlockingQueue和有界的BoundedBlockingQueue及Intel Threading Building Blocks的concurrent_queue<T>;
3.3 推荐模式
【推荐使用】:one(event) loop per thread + thread pool
;
event loop
即IO loop用作IO multiplexing,配合non-blocking IO
和定时器
;thread pool
用来计算
,处理任务队列或生产者消费者队列;- 分配资源是否需要考虑执行
特殊任务的线程
,如loggin;
3.4 进程间通信只用TCP
首选Socket
,主要是TCP;
【好处】:可以跨主机
,具有伸缩性;
【pipe应用场景】:写Reactor/event loop
时用来异步唤醒epoll_wait
调用;
TCPport由一个进程独占,且操作系统会自动回收监听端口
和socket文件描述符
,进程结束时会关闭所有文件描述符;
- port
独占
可防止程序重复启动,抢不到port;
【与IPC相比】:TCP可记录、可重现、可跨语言、可再生;
- 可使用
tcpdump
和Wireshark
解决进程间协议和状态争端,可分析性能; - 可用
tcpcopy
来压测; - tcp通信会有数据编排和非编排的开销,需要使用合适的
消息格式
,如Google Portocol Buffers;
4 分布式系统使用TCP长连接通信
通常一个分布式系统运行再多台机器的多进程组成,进程间采用TCP长连接通信
;
【长连接好处】:
- 容易定位分布式系统中的服务之间的依赖关系;
- 可通过
netstat -tpna | grep :port来查看服务客户端地址
,再客户端上用netstat
或lsof
可找出哪个进程发起的连接;这样可在迁移服务的时候能防止出现outage
;(TCP短链接及UDP没有该特性) - 通过
接收
和发送队列
的长度易定位网络或程序故障;- 运行时,
netstat -tn
打印的Recv-Q
和Send-Q
应该在0左右
; - 若
Revc-Q
不变或持续增加,则服务进程处理速度可能变慢,死锁或阻塞
; - 若
Send-Q
不变或持续增加,则可能服务器忙,来不及处理,或网络中丢包、掉线
;
- 运行时,
- 可通过
5 多线程服务器的使用场合
5.1 服务器基本任务
【处理并发连接】
- 若机器可创建
高于
CPU数目的线程,此时一线程只处理一个TCP连接(半个),一般使用阻塞IO
; - 若机器可创建线程
相当于
CPU数目,此时一个线程要处理多个TCP连接上的IO
,通常使用非阻塞IO和IO多路复用
;
【在处理并发连接时,要充分发挥硬件资源,当一台多核机器提供一种服务或执行一个任务,可用模式】:
- 【模式1】运行一个
单
线程的进程; - 【模式2】运行一个
多
线程的进程; - 【模式3】运行
多个
单线程的进程:- 【模式a】:把模式1中的进程运行多份;
- 【模式b】:主进程+woker进程;
- 【模式4】运行
多个多
线程的进程;
【使用速率为50MB/s的数 据压缩库、在进程创建销毁的开销是800μs、线程创建销毁的开销是 50μs的前提下,考虑如何执行压缩任务:】:
- 若
偶尔压缩1GB
的文件,时间是20s,则一个进程
去做是合理的,因为进程启动和销毁的开销远远小于实际任务的耗时
; - 若
经常压缩500kB
的文本,预计运行时间是10ms,那么每次都起进程似乎有点浪费了,可以每次单独起一个线程
去做; - 若要
频繁压缩10kB
的文本数据,预计运行时间是200μs,那么每次起线程似乎也很浪费,可直接在当前线程搞定
;也可以用一个线程池
,处理任务,避免阻塞当前线程
;
5.2 必须使用功能单线程的场合
【必须使用的场合】:
- 程序可能会
fork
;
【fork后的两种行为】:- 立刻执行
exec
,变为另一个程序或子程序运行fastcgi程序,或看门狗进程
(该程序必须为单线程); - 执行
当前程序
;
- 立刻执行
- 限制程序的
CPU占用率
;- 如一个8核服务器,开一个单线程出现了忙等待,占满一个核,在最坏情况下CPU使用率也就占到12.5%,还有剩余供其他服务使用,即使用单线程来避免抢夺系统资源;
【单线程的缺点】
- Event loop中,是
非抢占式
的,若a优先级高于b,a需要1ms来处理,b需要10ms;若b早于a发送,则当a事件唤醒时,程序已执行poll,并处理b,那么a只有等10ms后才有机会被处理,该情况为优先级反转
,可多线程来解决
;
【多线程下性能不一定能提升】
在很少的IO流量
下,让CPU跑满,用多线程无法提高性能
;
- 【1】对于静态web或FTP服务器,主要瓶颈在于
磁盘IO
和网络IO
;一个线程即可撑满IO,即在硬件容量上已达饱和,无法提高; - 【2】从n个整数总选出m个整数,和为0,计算该程序应选用
3a模式
;
5.3 适用多线程程序的场景
主要提高响应速度
、让IO
和计算重叠
、降低潜在因素
,可提高平均响应性能;
【满足条件】:
多个CPU
可用,单核机器上多线程没有性能优势;- 线程间
共享数据
,即内存中的全局状态,若没有共享数据,用模式3b; - 共享的数据是
可修改
,不是静态的常量表,若数据不能修改,则可在进程间用共享内存,适用模式3; - 事件响应有
优先级
差异,可用专门的线程来处理优先级高的事件,防止优先级反转; - 程序有
相对的计算量
,不是逻辑简单IO bound或CPU bound; - 利用
异步
操作,如日志,往文件或服务器都不应该阻塞; - 能搭配好增加CPU数目带来的好处;
- 具有
可预测
的性能; - 有效的划分
责任与功能
;每个线程逻辑任务单一,不能都塞到一个event loop;
【机群管理软件】:Linux服务器机群,8个计算节点,1个控制节点,需要3个程序:
- 运行在控制节点上的
master
,监视
控制机群状态
; - 运行在每个计算机节点上的
salve
,负责启动和终止job
,并监控本机的资源; - 供用户适用的client工具,用于提供job;
【其中】:
- salve为
看门狗进程
,会启动别的job进程,需要用单线程
,不需要占太多资源; - master应该为
模式2
的多线程程序:- 独占一个8核机器,若用
模式1
,则浪费87.5%的资源; - 整个机群的状态应该完全在内存中,状态时
共享且可变的若用模式3
,则进程间同步会是大问题,若用共享内存,则成了多进程的多线程,一个进程阻塞或crash,则其他进程死锁
; - master主要指标不是吞吐量,而是
延迟
,尽快响应事件
,不会出现IO/CPU跑满; - master监控的事件有
优先级
区别,一个程序正常运行结束核异常处理的优先级不同,计算节点的磁盘满
和机箱温度
优先级也不同,若单线程,则会出现优先级反转
; - 若master与每个slave用一个
TCP连接
,则master采用2或4个IO线程来处理8个TCP
连接可能有效降低延迟; - master要
异步
往本地硬盘写log,需要要求log lib有自己的IO线程; - master可能要
读写
数据库,则数据库连接第三方lib可能要有自己的线程
,并回调master代码; - master要服务器与多个clent,
用多线程降低客户响应事件
,即可再用2个IO线程专门处理和clents的通信; - master可提供一个monitor接口,用来
广播推送机群的状态
,即用户不需主动轮询;可用单独线程实现
;
【master一共开10个线程】:虽线程数>core数,但大多线程都是空闲,可依赖OS的进程调度来保证可控的延迟;
- 独占一个8核机器,若用
- 4个用于和slaves通信的IO线程;
- 1个logging线程;
- 1个数据库IO线程;
- 2个和clent通信的IO线程;
- 1个主线程,用于做背景工作;
- 1个pushing线程,用于主动广播机群状态;
【TCP聊天服务器】
聊天(人与人、机器与机器)
,服务器特点是并发连接之间数据交换,一个连接收到的data转发给其他多个连接,故只能用模式1、2
;
- 若只有
数据交换
,则模式1,CPU快
,单线程应付几百个连接也可以; - 若加上
关键字过滤,黑名单,防灌水
等每项功能都会占CPU,则需要考虑模式2
;若顺序处理可能导致消息发送延迟
,则将这些任务分散到多个线程中;
【服务器分类】:
IO线程
,主循环在IO multiplexing,阻塞等待epoll_wait上,也处理定时
事件,也可处理消息编码解码
等;计算线程
,主循环在阻塞
队列,阻塞等在条件变量
上,一般为线程池
中的线程,不涉及IO,避免阻塞
;第三方库
所用的线程,如loggin,database;
6 多线程服务器的适用场合
Linux能同时启动多少个线程
- 在32位Linux下,一个进程地址空间为4G,其中用户态为3G,而一个线程默认栈位10M,即一个进程大概可以同时启动300个线程;
多线程能提高并发度吗
- 若指的是
并发
连接,则不能;thread per connection
不适合高并发场合,其可扩展性不佳,one loop per thread
的并发度足够大,且与CPU数目成正比
;
多线程提高吞吐量吗
- 对于
计算密集型服务不能
; - 【例1】:若有耗时的计算服务,用单线程需要0.8s,在8核机器上可启动8个线程或进程,来一起计算可将吞吐量从1.25提升到10qps(实际可能打8折);
- 若用并行计算,用8核一起计算,若完全并行,则计算时间为0.1s,吞吐量仍为10qps,但首次请求响应慢,可能加速比也就只有6个,计算时间为0.133s;
- 【例2】:8核机器上压缩100个1G的文件,每个core处理能力为200M/s,可每次起8个进程,每个进程压缩一个文件或依次压缩每个文件,每个文件用8个线程并行压缩;两种耗时差不多,但第二种能较快地拿到第一个压缩完的文件,即首次响应的延时更小;(进程创建的速度慢)
- 若适用thread per request,每个客户请求用一个线程去处理,则并发请求数到达临界值,吞吐量将会下降,由于上下文切换需要开销;
- 为了并发请求数高也要保证稳定的吞吐量,则我们可用
线程池
,大小需要满足阻抗匹配
原则;- 若需要计算,且计算时间占请求时间的
1/5
,则用线程池是合理的; - 若一次请求响应,主要时间在
等待IO
,则为了进一步提高吞吐量,则需要使用其他模型,如Proactor;
- 若需要计算,且计算时间占请求时间的
多线程能降低响应时间?
- 需要
充分利用
多核资源;
【例1 计算密集型】:多线程处理输入以memcached服务端
为例,memcached一次请求响应为以下步骤:
- 读取并解析客户端输入;
- 操作hashtable;
- 返回客户端;
在单线程下,3步为串行执行
;
在多线程下,启用多个输入线程(4个)
,并在建立连接时按循环法把新连接
分派给其中一个输入线程
,即one loop per thread
模型;即第一步可多线程并
,提高速度,第二步需要锁
,还是单线程,可改进;
【例2 计算密集型】:多线程分担负载,若我们要左一个求解Sudoku的服务,这个服务程序在9981端口接受请求,输入一行81个数字,输出为填好后的81个数字(1~9)若无解,输出NO\r\n;
- 输入格式简单,只用单线程做IO即可;假设求解用时为10ms,用前面的方法计算,单线程能达到的吞吐量上限为100qps;在8核机器上,若用
线程池
来计算,能达到的吞吐量上限为800qps; - 【分析多线程如何快】:
- 单线程处理,reqs会排队处理缓冲区,第一个10ms……第十个100ms平均响应时间为55ms;
- 多线程中一个IO线程,8个计算线程,有任务进来即分配到空闲线程,平均响应为12ms;
多线程程序如何让IO和计算互相重叠,降低延迟
【基本思路】:把IO操作通过阻塞队列
交给别的线程去做;
【例1】:日志,在多线程服务器程序中,日志很重要,仅考虑log file;
在一次请求响应中,可能要写多条日志信息,而若用同步
方式写文件,多半会降低性能
;
- 【原由】:
- 文件操作
慢
,线程阻塞在IO
上,让CPU闲置,增加相应时间; - 若使用buffer来解决则需要
加锁
,让线程等待
,降低并发度;
- 文件操作
- 【解决】:用一个
单独
的loggin线程,负责写磁盘文件
,通过一个或多个阻塞队列对外提供接口;当别的线程要写日志时,先把消息准备好,往队列中加,即不用等待;
【例2】:memcached客户端,若我们用memcached来保存用户最后的发帖时间,则每次响应用户发帖的请求时,程序要去设置一下memcached里的值,若这里用同步IO,则会增加延迟;
- 对于设置一个值这样的
write-only idempotent
操作,不需要等待memcached返回操作结果,不需要在乎set操作是否失败,可借助多线程来降低响应延迟;- 如:我们写一个多线程版memcached客户端,对set操作,调用方只要把key和value准备好,调用给你set,将数据压入阻塞队列,剩下的事情交给memcached客户端的线程操作,服务端线程不受阻碍;
- 所有的网络写操作都可
异步
操作,但也有缺点:每次write都要再线程间传递数据,若TCP缓冲区为空,则我们可以再本线程写完,不要交给专门的IO线程(Netty使用该做法);
什么是线程池大小的阻抗匹配原则
- 若线程池的任务,密集计算占时比重为P(0<P<=1),而系统一共有C个CPU,为了让C个CPU跑满而不过载,线程池大小的经验公式:
T = C/P;T值上下浮动50%
; - 若
P<0.2
则T可以取固定值,则5xC
;C为分配给这个任务的CPU数目;
还有其他的non-trivial多线程编程模型?
【Proactor】:若一次请求响应中要和别的进程多打几次交道
,则用该模型可提高并发度
,但代码繁杂,不能降低延迟
;
【例1】:一次HTTP proxy的请求若没有命中本地cache,则:
- 解析域名,陌生可能会花几秒钟;
- 建立连接;
- 发送HTTP请求;
- 等待对方回应;
- 返回响应;
- 上述中2个server发生
3次往返
,每次可能都要花几百毫秒:- 向DNS问域名,等待回复;
- 向对方的HTTP服务器发起连接,等待TCP三路握手完成;
- 向对方发送HTTP request,等待对方response;
- 实际HTTP proxy本身运算量小,若使用
线程池
,则线程数将会很多,不利于管理; - 【方法1】:把域名已解析、连接已建立、对方已响应做成
event
,继续按Reactor
编程,即每次客户请求就不能用一个函数从头到尾执行完成,而要分成多个阶段
,并管理好请求的状态; - 【方法2】:使用回调函数,让系统把任务串起来,若用户收到请求,如没有命中本地缓存,则执行:
- 立刻发起异步的DNS解析startDNSResolve(),告诉系统在解析完之后调用DNSResolved()函数;
- 在DNSResolved()中,发起TCP连接请求,告诉系统在连接建立之后调用connectionEstablished();
- 在connectionEstablished()中发送HTTP request,告诉系统在收到响应之后调用httpResponsed();
- 最后,在httpResponsed()里把结果返回给客户。
- Proactor依赖系统或库来高效地调度子任务,子任务不会阻塞,因此能用比较少的线程达到很高的IO并发度;
模式2和模式3a该如何取舍
- 根据服务程序响应一次请求所访问的
内存大小
来取舍; - 若工作集较大,则用
多线程
,避免CPU cache换入换出
,影响性能; - 否则,就用单线程,便利;
- 【例】:若程序有较大的本地cache,用于缓存一些基础参考数据,几乎每次请求都会访问cache;
- 则使用多线程合适,因为可避免每个进程都自己留一份cache,增加内存使用;