linux上的线程标识

本文讨论了在Linux中,为什么pthread_t不适合作为线程标识符,推荐使用gettid()返回值作为线程ID。同时,介绍了线程创建和销毁的原则,以及线程销毁的不同方式。文章还提到了__thread关键字的使用,多线程与fork()、信号处理在多进程中的注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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
线程的创建和销毁的原则
  1. 程序库不应该在未提前告知的情况下创建自己的“背景线程”
  2. 尽量用相同的方式创建线程,例如 muduo::Thread
  3. 在进入 main() 函数之前不应该启动线程
  4. 程序中线程的创建最好能在初始化阶段全部完成
线程销毁有几种方式
  • 自然死亡。从线程主函数返回,线程正常退出
  • 非正常死亡。从线程主函数抛出异常,或线程触发 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 来通知其他线程
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值