Linux 多线程服务端编程读书笔记(四)

Linux多线程服务端编程笔记(四)

第四章 C++多线程系统编程精要

1、多线程编程最大的思维方式的转变有两点
  1. 当前线程可能随时会被切换出去,被抢占
  2. 多线程程序中事件的发生顺序不再是全局统一的先后关系
2、 多线程程序的正确性
  1. 不能依赖任何一个线程的执行速度
  2. 不能通过原地等待(sleep)来假定其他线程事件已经发生,必须通过适当的同步来让当前事件能够看到其他线程执行的结果
  3. 无论线程执行的快慢,程序都能正常工作
3、 基本线程原语的选用
  1. 2个:线程的创建和等待结束(jion),封装成muduo::Thread;
  2. 4个:mutex的创建、销毁、加锁、解锁
  3. 5个:条件变量的创建、销毁、等待、通知、广播
4、 C/C++系统库的线程安全性
  1. 不用担心系统调用的线程安全性、因为系统调用对于用户态程序来说是原子的。但是需要注意它的使用对内核状态的改变可能会影响其他线程
  2. C++标准容器库和std::string 不是线程安全的,只有std::allocator是线程安全的
  3. C++库中的绝大多数泛型算法是线程安全的
  4. C++的iostream不是线程安全的
5、Linux上的线程标识
  1. pthread_self 函数用于返回当前线程的标致符,类型为pthread_t

    注意pthread_t:

    • 无法打印输出,不知道确切类型,因此日志中无法用它表示当前线程
    • 无法比较大小或计算其hash值
    • 无法定义一个非法的pthread_t值来表示不可能存在的线程id
    • 只在进程中有意义,与操作系统的任务调度之间无法建立有效管理
  2. pthread_equal 对比两个线程标志符是否相等

  3. Linux系统上建议gettid(2)系统调用的返回值来作为线程的id,原因如下

    • 类型是pid_t,是一个小整数, 便于在日志中输出

    • 它直接表示任务调度id,在/proc文件系统中可以轻易找到对应项

    • 其他系统工具也容易定位到具体某一个线程,top(1)

    • 任何时刻都是全局唯一的,分配新的pid是递增轮回办法

    • 0是非法的,系统第一个进程init的pid是1;

4、线程创建与销毁的守则

线程的创建原则:

  1. 程序库不应该在为提前告知的情况下创建自己的“背景线程”
  2. 尽量用相同的方式创建线程
  3. 进入main函数之前不应该启动线程
  4. 程序中线程的创建最好在初始化阶段全部完成

线程的销毁方式

  1. 自然死亡。从线程的主函数返回,线程正常退出

  2. 非正常死亡,抛出异常或触发致命信号

  3. 自杀。 自己调用pthread_exit()退出

  4. 他杀。其他线程调用pthread_cancle()

    注意,线程正常退出的方式只有一个,自然死亡,任何从外部强行终止线程的做法和想法都是错误的

6、exit(3)在C++中不是线程安全的

​ exit(3)函数在C++中的作用除了终止线程,还会析构全局对象和以及构造完成的函数静态对象。这有存在死锁的可能

7、善用__thread关键字
  1. __thread是GCC内置的线程局部存储设施,他的实现非常高效,存取速率可以与全局变量相比
  2. 使用规则:智能修饰POD(普通旧类型)类型,不能修饰class类型,因为无法调用构造函数与析构函数
  3. __thread变量是每个线程有的一份实体,各个线程的变量互不干扰。除此之外,还可以修饰那些值可能会改变,带有全局性,但是又不值的用全局锁保护的变量
8、多线程与IO
  1. 每个文件描述符只由一个线程操作,从而轻松解决消息收发的顺序性问题,也避免了关闭文件描述符的各种 race condition。一个线程可以操作多个文件描述符,但一个线程不能操作别的线程拥有的文件描述符。
  2. epoll也遵循相同的原则,把同一个epoll的操作(添加、删除、修改、等待)都放到同一个线程中执行
  3. 一般的程序不会直接使用epoll、read、write,这些底层操作由网络库代劳
9、用RAII包装文件描述符
  1. linux的文件描述符是小整数,程序刚启动时候,0是标准输入,1是标准输出,2是标准错误。因此新打开的文件描述符是3

  2. POSIX标准要求每次打开文件的时候必须是当前最小可以文件描述符符号,因此这种方式会造成串话

    比如:

    • 第一个线程read某个socket,第二个线程几乎同时close此socket,第三个线程又打开了另一个文件描述符,与之前的相同,则会导致第一个线程会读不到属于他的数据
    • fd = 8收到了比较耗时的请,开始处理这个请求,并记住将响应结果给fd = 8,.但是在处理过程中fd = 8 断开连接,于是在处理完成之后,返回响应已经物是人非了

    解决这个问题的办法

    • 单线程程序,使用某种全局表
    • d多线程程序:RAII,用Socket对象包装文件描述符,只要socket还活着,就不会有其他的Socket对象有相同的文件描述符
10、 RAII与fork()
  1. fork()之后,子进程会继承父进程的几乎所有的状态,但也有少数例外

  2. 子进程会继承地址空间和文件描述符,因此用于管理动态内存和文件描述符的RAII class 都能正常工作,但是子进程不能继承:

    • 父进程的内存锁
    • 父进程的文件锁
    • 父进程的某些定时器
    • 其他

    由此得出结论,RAII技法与fork冲突

11、多线程与fork()
  1. fork一般不能在多线程程序中调用,因为fork只克隆当前线程的thread of control。不克隆其他线程。因此fork之后,除了当前线程,其他线程就消失了
  2. 唯一安全的做法,fork之后立即调用exec()执行另一个程序,彻底隔断子进程与父进程的联系
  3. windows下的创建创建进程与当前进程关联要比较少
12、多线程与signal
  1. linux信号与多线程水火不容
  2. signal会打断正在运行的线程控制(thread of control)
13、本章总结

​ 多线程C++编写原则

  1. 线程时宝贵的,一个程序可以使用几个或几十个线程,但一台机器不应该同时运行几百个,几千个线程,会增加内核的负担
  2. 线程的创建和销毁是有代价的,最好一开始就创建所有线程反复使用,如果必须那么做,最好是一分钟一次
  3. 每个线程都应该有明确的职责,例如IO线程或计算线程
  4. 线程之间的交互应该尽量简单,理想情况下只用消息传递,如果必须使用锁,最好避免同一个线程拥有两把锁
  5. 考虑清楚一个mutable shared 对象会暴露给哪些线程,每个线程是读还是写,是否并发
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值