pthread_t不适合用作线程标识符
POSIX theads 库提供了 pthread_self 函数用于返回当前进程的标识符,其类型为 pthread_t 。 pthread_t 不一定是一个整数类型,也有可能是一个结构体,因此 pthread 专门提供了 pthread_equal 函数用于对比两个线程标识符是否相等,这就带来一系列问题,包括:
- 无法打印 pthread_t
- 无法比较 pthread_t 的大小或计算其 hash 值,一次无法用作关联容器的 key
- 无法定义一个非法的 pthread_t 值,用来表示绝对不可能存在的线程 id ,因此 mutexLock 没有办法有效判断当前线程是否已经持有本锁
- pthread_t 的值只在进程内有意义,与操作系统任务调度之间无法简历有效关联。比如说在 /proc 文件系统中找不到 pthrea_t 对应的 task
使用 gettid() 系统调用的返回值作为线程 id
好处:
- 他的类型是 pid_t,其值通常是一个小整数
- 在现代 Linux 中,它直接表示内核的任务调度 id,因此在 /proc 文件系统中可以轻易找到对应项:/ proc / tid 或 / proc / task / tid
- 在其他系统工具中也容易定位到具体某一线程,例如 top 中我们可以按线程列出人物,然后找出 CPU 占用率最高的线程 id
- 任何时刻都是全局唯一的,并且由于 Linux 分配新 pid 采用递增轮回办法,短时间内启动的多个线程也会具有不同的线程 id
- 0 是非法值,因为操作系统的第一个进程 init 的 pid 是 1
线程的创建和销毁的原则
- 程序库不应该在未提前告知的情况下创建自己的“背景线程”
- 尽量用相同的方式创建线程,例如 muduo::Thread
- 在进入 main() 函数之前不应该启动线程
- 程序中线程的创建最好能在初始化阶段全部完成
线程销毁有几种方式
- 自然死亡。从线程主函数返回,线程正常退出
- 非正常死亡。从线程主函数抛出异常,或线程触发 segfault 信号等非法操作
- 自杀。在线程中调用 pthread_exit() 来立刻退出线程
- 他杀。其他线程调用 pthread_cancel() 来强制终止某个现场
善用 __thread 关键字
__thread 是 GCC 内置的线程局部存储设施,存取效率可比肩全局变量
thread 使用规则:
只能用于修饰 POD 类型,不能修饰 class 类型,因为无法自动调用构造函数和析构函数。__thread可以用于修饰全局变量、函数内的静态变量,但是不能用于修饰函数的局部变量或者 class 的普通成员变量。另外,__thread变量的初始化只能用编译器常量,例如:
__thread string s("abc") //错误,不能调用对象的构造函数
__thread string *p= new string //错误,初始化必须用编译器常量
__thread string *p=null //正确,但是需要手工初始化并销毁对象
__thread 变量时每个线程有一份独立实体,各个线程的变量值互不干扰。除了这个用途,他还可以修饰哪些“值可能会变,带有全局性,但是又不值得用全局锁保护”的变量
多线程与 fork()
多线程与 fork() 协作性很差
fork() 一般不能在多线程程序中调用,因为 Linux 的 fork() 只克隆当前线程的 thread of control ,不克隆其他线程。fork() 之后,除了当前线程之外,其他线程都消失了。也就是不能一下子 fork() 出一个和父进程一样的多线程子进程。Linux 没有 forkall() 这样的系统调用 forkall() 其实也是很难办的,因为其他线程可能等在 condition variable 上,可能阻塞在系统调用上,可能等着 mutex 以跨入临界区,还可能在密集的计算中,这些都不好全盘搬到子进程里
fork() 之后子进程中只有一个线程,这就造成一个危险的局面。其他线程可能正好位于临界区内,持有了某个锁,而它突然死亡,再也没有机会去解锁了。如果子进程试图再对同一个 mutex 加锁,就会立刻死锁。在 fork() 之后,子进程就相当于处于 signal handler 之中,你如能调用线程安全的函数(除非它是可重入的),而只能调用异步信号安全的函数,比方说,fork() 之后,子进程不能调用:
- malloc() 。 因为 malloc 在访问全局状态时几乎肯定会加锁
- 任何可能分配或释放内存的函数,包括 new 、map::insert()、snprintf ……
- printf 系列函数,因为其他线程可能恰好持有 stdout/stderr 的锁
- 除了 man 7 signal 中明确列出的 ‘signal 安全’ 之外的任何函数
如此看来,唯一安全的做法是在 fork() 之后立刻调用 exec() 执行另一个程序,彻底隔断子进程与父进程之间的关系
多进程与 signal
单线程时代
- 由于 signal 打断了正在运行的 thread of control ,在 signal handler 中只能调用 async-signal-safe 的函数
- 如果 signal handler 中需要修改全局数据,那么被修改的变量必须是 sig_atomic_t 类型的。否则被打断的函数在恢复执行后很可能不能立刻看到 signal handler 改动的数据,因为编译器有可能假定这个变量不会被他处更改,从而优化了内存访问。
多线程时代
信号分为两类:发送给某一线程、发送给进程中的任一线程。
特别是 signal handler 中不能调用任何 pthread 函数,不能通过 condition variable 来通知其他线程