【阅读笔记】Linux多线程服务端编程(1)

多线程系统编程

对象生命周期管理

线程安全定义

  • 多个线程同时访问,行为正确,无论操作系统如何调度
  • 调用端代码无须额外同步操作

基本原则:凡非共享的对象彼此独立,只被一个线程用到;而const共享对象的read-only是线程安全的

对象构造与销毁

构造唯一要求:构造时不要泄露this指针
构造期间还没有完成初始化,别的线程可能访问到这个半成品。最后一行也不行,如果是个基类呢,基类构造虽然完成,但派生类还有构造函数呢。
二段式构造——构造函数+initialize()可能会是个好的选择

销毁比较麻烦:
对象本身的mutex并不能保护临界区,因为析构时mutex也会被销毁。例如析构时如果用mutex保护,加了锁,另一个线程访问另一个成员函数,因为同样访问临界区然后也会加锁,这时析构函数完成,锁没了!
所以mutex不能保护析构

同时读写两个对象,可能会发生死锁,例如同样访问a,b,线程A先锁a,线程b先锁b,那就死锁了。
如果一定要锁住相同类型多个对象,需要保证始终按相同顺序加锁,例如通过mutex的地址顺序

拥抱智能指针吧!?

C++里可能出现的内存问题:

  • 缓冲区溢出
  • 悬垂指针/野指针
  • 重复释放
  • 内存泄漏
  • 不配对的new[]/delete
  • 内存碎片
    智能指针(shared_ptr, weak_ptr, unique_ptr)可以解决前5个问题

但需注意:

一般来说,智能指针应该是栈元素。

智能指针所管理的对象是线程安全的,但智能指针本身并不是,所以访问智能指针时需要加锁

需注意可能的意外延长对象生命周期,存储智能指针时应该是weak_ptr, 而不是shared_ptr,另外需注意bind()时可能的拷贝

智能指针拷贝开销更高一些,建议一个线程只在最外层函数有一个实体shared_ptr,之后通过引用传参

在无须共享对象时,使用unique_ptr似乎是个好选择(独占式)

多线程编程应当最低限度地共享数据,这才是根本,其次考虑不可修改的数据,再次才暴露可修改数据,并提供同步机制

线程同步

只使用非递归互斥器(不可重入)和条件变量,慎用读写锁,不用信号量
正确比快更重要,应当首先追求正确,再去考虑性能

永远不要用sleep来“同步”

不推荐使用读写锁和信号量

读者-编者问题
读写锁不一定比普通mutex快,因为要更新reader数目
追求高性能:read-copy-update方式(RCU方式),无锁
copy-on-other-reading:作者在2.8节的方式,利用shared_ptr的计数和mutex实现读写锁功能,当有读者时,复制一份,然后将副本和旧数据交换,在真实数据上修改,不影响其余读者读取数据。这个过程读者临界区只需要获得数据就行了,临界区很小。

信号量并没有互斥锁+条件变量灵活好用,而且信号量本身也有一个计数,同我们的数据计数重复了。

编程模型

进程拥有独立的地址空间,可以把每个进程比喻为一个人,每个人都有自己的记忆,但不知道别人的记忆。然后就可以思考:容错、扩容、负载均衡、退休等等场景

单线程模型:“non-blocking IO + IO mutiplexing”基于事件驱动的编程模型要求回调函数必须是非阻塞的,容易割裂业务逻辑,不容易理解和维护(现代语言的协程是一种应对思路)

多线程模型:
one loop per thread + 每个请求创建一个/线程池 + 阻塞式/非阻塞式IOmutiplexing
领导者/跟随者模式

推荐模式:one loop per thread + 线程池 + nonblocking mutiplexing

进程间通信作者只推荐TCP,不建议共享内存。TCP代码复杂度低,支持场景多(比如跨设备),而共享内存需要a填好一块内存让b来读,算是“停等”,二者结合起来比较麻烦,复杂度高。
我觉得只有在写少读多的场景下共享内容才有优势。

适用场合

模式:

  1. 单进程单线程
  2. 单进程多线程
  3. 多进程单线程
    a. 把模式1的进程运行多份
    b. 主进程+worker进程
  4. 多进程多线程

单线程场合:

  • 需要fork,最好单线程,多线程fork会很麻烦(linux fork只会fork当前线程)。目前唯一需要这样的场合只有守护进程相关(看门狗进程,如集群中运行在计算节点上的负责启动job的守护进程)
  • 限制CPU占用率。非关键任务不应该使用全部资源,否则有可能造成关键任务资源不够造成响应慢

多线程场合:
概括:提高响应速度,让IO和计算重叠

  • 前提是有多个CPU核心可用
  • 有共享数据且可修改,相比于模式3,这时候用多线程可降低内存消耗及通信开销
  • 优先级,有些事件优先级较高,应及时响应
  • 响应时间(latency延迟)平均,相比于模式1
  • 划分功能

线程划分:

  • IO线程
  • 计算线程
  • 第三方库线程,如logging或者数据库连接

线程池大小设计:
阻抗匹配原则:密集计算所占事件比重*线程数=CPU核心数
比重小于0.2不再适用(可能是因为线程切换开销?)

多线程系统编程

思维转变:线程随时会切换出去(被抢占),事件发生顺序不再有全局统一的先后关系。
基本线程原语:线程、互斥锁、条件变量

c/c++系统库:

  • glibc库函数大部分是线程安全的
  • 线程安全是不能组合的,两个线程安全的操作合在一起就不一定了
  • C++中的流式操作不是线程安全的,因为等同于多个函数调用(同理,如果一个对象重载了操作符,那么连加连等之类的也不是线程安全)
  • posix规定的pthread_t并不一定是个整形,只是glibc中是unsigned long,实际会当做一个struct看待。同样pthread_t在时间上看并不具有唯一性,前一个线程结束后再创建的线程可以跟之前线程pthread_t相同
    • 作者建议使用gettid()的返回值作为线程ID

线程管理:

  • “线程是稀缺资源”,这里的线程指的是真实的由操作系统调配的线程
  • 统一管理线程,程序初始阶段创建全部工作线程,运行期间不再创建和销毁
  • 线程销毁唯一的正确方式是自然退出
    • 其他方式比如异常、自杀pthread_exit、他杀pthread_cancel等
    • 安全退出很难实现,exit(3)(析构全局对象和函数静态对象)可能会导致死锁

善用__thread

  • 修饰POD类型,可修饰全局变量和静态变量,每个线程复制一份,互不影响,访问效率高
    • 这个“每个线程复制一份”,指的是在新线程中该变量会重新初始化,不受之前线程赋值的影响
  • 不能修饰class类型,因为无法自动调用构造函数和析构函数

多线程与IO:

  • 大部分情况下,一个文件描述符应当只由一个线程操作(磁盘文件和UDP可例外)
  • 串话问题:A在使用fd8,然后B关闭了fd8,又重新创建了fd8,A从fd8获得了意外的数据,并且妨碍了新的fd8的通信。
    • 使用RAII管理connect,同一个fd只由一个connect对象操作

其他:
-不要使用signal,也不要使用基于signal的定时函数,不处理异常信号(除了SIGPIPE)。
- 传统方法是在信号处理函数中写入pipe,然后由IO事件处理框架统一处理
- 现代方式可使用signalfd,避免使用signal handler

  • 创建文件描述符可直接指定O_NONBLOCK和FD_CLOEXEC,分别表示非阻塞和exec时关闭描述符,防止泄露。

日志

两个意思,一个是诊断日志供人阅读,一个是交易日志供回滚状态,本章指诊断日志

Log Everything All The Time

在日志角度考虑,将日志处理分为前端和后端,前端负责写入缓冲区,后端负责将日志刷到磁盘。

功能:

  • 多种级别,运行时可调
  • 多个目的地(分布式系统只有本地文件,因为网络日志不可靠)
  • 格式可配置
    • 一条日志应当只有一行,这样方便搜索和处理
    • 时间戳-微妙
    • 线程id
    • 日志级别
    • 原文件名和行号
  • 过滤器
  • rolling
    • 例如写满1GB换下一个文件
    • 例如定时换下一个文件

性能:

前端不阻塞正常执行流程,性能损失要小
后端吞吐量要足够大
双缓冲技术

性能不能仅靠感觉,比如有时直接拷贝数据比传递指针还要快

问题

  1. P15-如果智能指针时某对象的数据成员,模版参数T又是imcomplete类型,那么该对象的析构函数需显示定义,不能用默认的
  2. 一个POD类型需要满足什么条件?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值