linux C++多线程服务端开发
UNIX
线程安全的对象生命期管理
当析构函数遇到多线程
构造
- 不要在构造函数中注册任何回调
- 不要在构造函数中把this传给跨线程的对象
- 即便在构造函数的最后一行也不行,因为这个类可能是基类,构造完基类还要构造派生类,就是怕对象未构造完成,却暴露且使用了this
销毁太难
- 如果线程A对x对象要执行析构,线程B对x要调用update函数,会出问题。
- 如果用mutex,也不行,比如A获得了互斥锁,进而进行了析构,B虽然阻塞在锁上在等待A释放,但是A已经析构了,B是继续阻塞?天晓得接下来发生什么。
- delete之后,把相关变量置为NULL,也是没用的。
作为数据成员的mutex不能保护析构
- mutexLock可以保证读写正确,但是无法保证析构正确
- 只有别的线程都访问不到这个对象的时候,析构才安全
- 如果要同时读这个类的两个对象,有可能发生死锁,比如两个线程分别执行swap(a,b),swap(b,a);
线程安全的Observer有多难
- 一个动态创建的对象是否还或者,光看指针是看不出来的,引用也一样。如果已经销毁就不可能根据里面的状态判断,更不能根据这个指针的值(万一又创建了呢,这种方法是C指针问题的根源)。
- 简单的方法是只创建不销毁,不够就开辟,用完就放回去。这样至少可疑避免访问失效对象的情况。但是这种山寨方法的问题有:
全局共享数据引发的lock contention。
对象池的线程安全,如何完全,安全把对象放回去
- 现在要解决的问题是用的时候一定知道对象还活着
- 要想安全销毁对象,最好在别人(线程)都看不到的情况下,偷偷地做,垃圾回收的原理就是这个
空悬指针
jj
线程同步
线程同步四项原则,按照重要性排列:
- 最低限度共享对象,较少需要同步的场合。
- 使用高级的并发编程构件
- 最后使用底层同步原语。使用互斥器和条件变量,慎用读写锁,不要用信号量。
互斥器
- 用RAII手法封装mutex的创建,销毁,加锁,解锁。
- 只用非递归的mutex
- 不手工调用lock,unlock,一切交给构造和析构函数
- 不适用跨进程的mutex,只用tcp sockets
- 加锁,解锁在同一线程
条件变量
解决互斥锁阻塞的问题
int dequeue() {
lock(mutex);
while(queue.empty())
{
cond.wait();
}
int top = queue.front();
queue.pop_front();
return top;
}
void enqueue(int x){
lock(mutex);
queue.push_back(x);
cond.notify();
}
读写锁
效率不比mutex快。
信号量
条件变量和互斥锁的结合可以替代其功能,而且不易用错。
多线程服务器的适用场合与编程模型
- 多线程可以共享数据,而且更好的利用多核。
- 单核服务器利用多线程每多少价值
多线程常见模型
- 每个请求创建一个线程,使用阻塞是I/O。
- 适用线程池,同样使用阻塞IO,与第一种相比,提高了性能。
- 适用non-blocking IO + IO multiplexing 即,Java NIO
- leader/follower 等高级模式
默认情况下是第三种。
one loop per thread
推荐使用one loop per thread + thread pool
进程间通信的方式很多,pipe,FIFO,共享内存,消息队列,还有一些同步原语。
推荐使用TCP,因为如果多进程依然无法满足服务,就需要扩充到其他机器,易于扩展
如果用很少的cpu负载就能让IO跑满,或者用很少的IO流量就可疑让cpu跑满,那么多线程没啥用处。
- 静态web服务器,FTP服务器,cpu的负载很小,主要瓶颈在磁盘IO和网络IO,这时候一个单线程的程序就可疑撑满IO。多线程并不能提高吞吐量。
- cpu跑满很少见,比如m个数,找出n个数,使其和为0.,能把cpu算死。这种情况,多启动几个单线程的进程就可疑了。
适合多线程的场景
虽然多线程不能提高绝对性能,但能提高平均响应性能。
- 多核
- 有数据共享,如果没有数据共享,则用多个进程解决
- 数据可以修改,如果数据都是敞亮表,就可以在进程间用shared memory,多个进程(每个进程一个线程)
- 提供非均质的服务,即,时间的相应有优先级差别,可以用专门的线程处理优先级高的
- 一个好的多线程程序,应该可以享受cpu数目增加带来的好处
- 32位的机器,4G的地址空间,用户态可以访问3G,如果不修改栈的调用空间10M,300个线程
疑难解答
多线程日志
对于C++程序,最好整个程序使用相同的日志库,程序有一个正体的日志输出,而不要各个组件有各个组件的日志输出。一般的日志风格有两种
- 借助printf();,log_info("");
- 借助stream<<,LOG_INFO << ""
日志的功能需求
因为日志库不能每条消息都flush到硬盘,也不能每条日志都open/close文件(开销太大)。方法
- 定期(默认3秒)将缓冲区的日志消息flush到硬盘
- 每条日志消息都带有一个cookie(或者叫哨兵值/sentry),其值是某个函数地址,这样通过coredump文件可疑找到cookie,这样就可以找到尚未写到磁盘的消息。
日志消息格式要点:
- 每条日志占一行。
- 时间戳精确到微妙。
- 打印线程id,便于分析多线程的时序,也可以检测死锁。
- 日志级别。
- 文件的名字行号。
日志的性能需求
日志库要足够高效。输出足够多的诊断信息,减小运维难度,提升效率。
- 每秒几千上万条日志没有明显的性能损失。
- 能应对一个进程产生大量日志的场景,比如1GB/min
- 不阻塞正常的执行流程
- 在多线程程序中,不造成争用。
性能指标
- 磁盘带宽约110MB/s,日志库应该能瞬间写满这个带宽。
- 假如每条是110字节,意味着每秒要写100万条
多线程异步日志
用多个buffer,缓冲
muduo网络库简介