Chapter04_学习

发布时间 2023-05-26 02:59:41作者: Stitches

多线程安全

现在 glibc 库函数大部分是线程安全的,特别是 FILE* 系列函数是安全的,但是两个或者多个函数组合到一起就不是安全的了。例如 fseek()fread() 两个函数都是线程安全的,但是对某个文件先 seek()read() ,这两步操作中间有可能会被打断,其它线程有可能趁机修改了文件指针的位置,让程序逻辑无法执行。


Linux上的线程标识

pthread_t 存在的问题

POSIX threads 库提供了 pthread_self 函数用于返回当前进程的标识符,其类型为 pthread_tpthread_t 不一定是一个数值类型,也可能是一个结构体,因此库采用了 pthread_equal() 函数来判断两个线程标识符是否相等,但这个引来了一系列问题:

  • 无法打印 pthread_t,因为不知道其确切类型;
  • 无法比较 pthread_t 的大小或者计算其哈希值,因此无法作用到关联容器的 key;
  • 无法定义一个非法的 pthread_t 值,用来表示绝不可能存在的线程id,因此无法有效判断当前线程是否已经持有本锁;
  • pthread_t 只在当前进程有意义,无法和操作系统的任务调度建立关联。比如说 /proc 文件系统中找不到 pthread_t 对应的 task。

glibc 的 Pthreads 实际上把 pthread_t 用作一个结构体指针(unsigned long),指向一块动态分配的内存,而且内存是反复使用的。Pthreads 只保证了同一个进程内,同一时刻各个线程的 id不同,无法保证同一进程先后多个线程具有不同的 id,更不用说一台机器上不同进程间 id 的唯一性了。


如何解决?

在 Linux 上建议使用 gettid() 系统调用的返回值作为线程id,这么做的好处如下:

  • 类型为 pid_t,值为一个整数,便于在日志中输出;
  • 现代 Linux 系统中,它表示内核的任务调度 id,因此在 /proc 文件系统中可以轻易找到对应项:/proc/tid 或者 /prod/pid/task/tid
  • 任何时刻都是全局唯一的,并且由于 Linux 分配新 pid 采用递增轮回办法,短时间内启动的多个线程也会具有不同的线程 id;
  • 0 作为非法值,因此操作系统的第一个线程 init 的 pid 是1。

如果每次获取当前线程ID时都选择调用系统调用 gettid(),那么会很浪费,这里可以选择采用 __thread 变量来缓存 gettid() 的返回值,这样只有在本线程第一次调用时才会进行系统调用,以后都是直接从 thread local 的缓存中取,效率无忧。多线程程序在打印日志的时候可以在每一条日志消息中包含当前线程的 id,不必担心有效率损失。


线程的创建于销毁的守则

线程创建需要注意点:

  • 程序不应该在未提前告知的情况下创建自己的背景线程;
  • 尽量使用相同的方式创建线程;
  • 在进入main函数之前不应该启动线程;
  • 程序中线程创建最好能够在初始化阶段全部完成;

当线程在强制终止(无论自杀还是他杀),都没有机会清理资源,那么就没有机会释放已经持有的锁。

所以我们需要在 muduo::Thread 类中通过 pthread_detach(pid) 来等待线程结束并释放线程资源。


exit 线程不安全

exit 函数在 C++ 中除了终止进程,还会析构全局对象和已经构造完的函数静态对象,这可能会导致潜在的死锁。

GlobalObject::doit() 调用了 exit(),触发全局对象 g_obj 的析构。GlobalObject 的析构函数会试图加锁 mutex_,而此时 mutex_ 已经被 GlobalObject::doit() 锁住了,造成了死锁。

如果在某些场景下确实需要结束线程,采用 _exit 系统调用,它不会试图析构全局对象,也不会执行缓冲区刷新工作。