0 Thread Safe VS Reentrant

0.1 Thread Safe Functions

引用于 CSAPP section 12.7.1:

Thread-safe 函数:被多个线程反复调用也可以保证正确的结果

thread unsafe 函数的类型包括:

  1. Functions that do not protect shared variables
  2. Functions that keep state across multiple invocations
  3. Functions that return a pointer to a static variable
  4. Functions that call thread-unsafe function

大部分C库的函数是thread-safe的函数。下面列举了某些的thread-unsafe函数以及它们的thread-safe版本:

thread-unsafe fnctions

0.2 Reentrant Functions

引用于 CSAPP section 12.7.2:

Reentrant 函数:不会引用任何shared data(包括global variable, static local variable)的函数

引用于 APUE section 10.6:

The Single UNIX Specification specifies the functions that are guaranteed to be safe to call from within a signal handler. These functions are reentrant and are called async-signal safe by the Single UNIX Specification. Besides being reentrant, they block any signals during operation if delivery of a signal might cause inconsistencies

SUS定义的可重入函数包括:

reentrant-functions

不在上表中的函数大致分为以下几类:

  1. 使用了static data的函数
  2. 调用了 malloc 或者 free
  3. 标准I/O库的函数,因为大部分标准I/O库的实现都用了全局数据结构

需要注意的是,即使调用了可重入函数,程序中的 errno 还是只有一个,是一个shared data.所以:

  1. 对于在signal handler中调用会影响到 errno 的函数要在调用前先保存原始的值,在调用完毕后恢复
  2. 对于多线程的情况,貌似无解

0.3 Thread Safe VS Reentrant

引用于 CSAPP section

venn diagram

1 多线程(带锁)

这里的“锁”指代各种形式,包括:semaphore, thread mutex,…

编程注意要点:

  1. (general) 不同的线程对于共用的锁的上锁顺序要一致
  2. (general) 线程中使用 thread safe 的函数

2 多线程(带锁)+ 信号(带锁)

这里的“锁”指代各种形式,包括:semaphore, thread mutex,…

根据处理信号的方式不同,大致分为两种。

2.1 信号处理函数(signal handler)

编程注意要点:

  1. (general) 不同的线程对于共用的锁的上锁顺序要一致
  2. (general) 线程中使用 thread safe 的函数
  3. (general) 如果要block某个信号,要用pthread_sigmask()而不能用sigprocmask(),因为后者在多线程的情景下是未定义的
  4. 对于signal的disposition(DFL, IGN, Catch)由某个线程统一的管理,因为任意一个线程改变了某个信号的disposition,整个进程都被影响
  5. 信号处理函数中使用reentrant函数
  6. 同一个线程不要连续lock同一个锁

    听起来比较不太可能犯的错误,不过在多线程与信号处理函数结合的情景下就可能发生,因为默认情况下信号(除了由硬件引起的信号)会中断任意线程。例如:

    1. 问题:对于一个信号的情况,可能被信号占用的线程, 信号处理函数 中用到的锁和 线程 中用到的锁一样的时候,可能会死锁

      举例:线程在lock mutex1的之后,unlock之前,被信号中断并占用当前线程,如果此时在信号处理函数中再次lock mutex1,就会死锁。

      解决:

      • 可能被中断的线程 不要有锁的操作

        • 例如,起一个专门的线程去处理这个信号,在这个线程的入口函数中执行简单的wait操作(例如:sleep(),…)
      • 信号处理函数 中不要有锁的操作
      • 可能被中断的线程信号处理函数 中没有共用的锁
      • 如果使用的是互斥锁,可以将它的 类型 设置为 PTHREAD_MUTEX_RECURSIVE
    2. 问题:对于多个信号的情况,可能被多个信号同时占用的线程,即使 线程 没有用锁,如果不同信号的 信号处理函数 用到同一个锁,也可能死锁

      举例:线程在执行过程中,被SIG1中断并占用,SIG1的信号处理函数代码中lock mutex1,在mutex1被unlock之前,该线程被SIG2中断并占用,SIG2的信号处理函数代码中也lock了mutex1,就会死锁。

      解决:

      • 可能被中断的线程 不要有锁的操作

        • 例如,如果多个信号需要同时被等待,那么就起多个线程去处理不同的信号(每个线程除自己等待的信号外,BLOCK其他信号),在这些线程的入口函数中执行简单的wait操作(例如:sleep(),…)
      • 信号处理函数 中不要由锁的操作
      • 可能被中断的线程 和 不同信号的 信号处理函数 中没有共用的锁
      • 如果使用的是互斥锁,可以将它的 类型 设置为 PTHREAD_MUTEX_RECURSIVE

    为了避免这种情况,比较通用和安全的做法是:

    1. 在创建任意线程前(包括可能在第三方库函数的调用中创建线程),在信号屏蔽字中block需要catch的信号。保证所有线程都不会被信号中断
    2. 如果有N个可能同时存在的信号,创建N个对应的线程,在每个线程中将对应的信号屏蔽字unblock,然后处于无限循环(while+sleep),在循环中不要调用其他可能会有锁操作的函数。保证每个信号只能中断某个特定的线程

    缺点:当有多个信号的时候,资源比较浪费

2.2 信号通过Block and sigwait的方式捕捉

使用sigwait()函数可以将原本异步的操作进行同步化。sigwait()的一般使用方式为:

  1. block所有线程信号屏蔽字中的需要catch的信号
  2. 起一个专门的线程,构造需要catch的信号集,在一个无限循环中以该信号集调用sigwait(),线程被阻塞
  3. 当需要catch的信号集中的某个信号generate之后,由于它是被block的,所以会处于pending状态。此时,sigwait()会原子地“将该信号的屏蔽字unblock,使该信号被deliver,恢复该信号的屏蔽字,返回”
  4. sigwait()后面的代码可以根据返回的信号做相应的操作(从这时直到下一次sigwait(),如果有其他信号产生,它们会处于pending状态)

缺点: 不同信号的处理不能并行化。

编程注意要点:

  1. (general) 不同的线程对于共用的锁的上锁顺序要一致
  2. (general) 线程中使用 thread safe 的函数
  3. (general) 如果要block某个信号,要用pthread_sigmask()而不能用sigprocmask(),因为后者在多线程的情景下是未定义的
  4. 对信号的disposition,使用DFL的而不要捕捉或者IGN该signal。 因为,如果一个信号会被捕捉并且一个线程正在sigwait(),那么最终用哪种方式deliver该信号是取决于实现的;如果一个信号被IGN,那么它没法被block,也就没法被sigwati()
  5. 保证在调用sigwait()前,必须block要wait的信号。否则,在sigwait()调用期间如果有signal产生,它们会丢失。

3 注意

  1. 项目中用到的第三方库函数,要考虑到它们是否 线程安全 ,如果线程安全,才可以用于多线程的情况
  2. 项目中用到的第三方库函数,要考虑到它们是否 可重入 ,如果不可重入,则避免在信号处理函数中使用
  3. 项目中用到的第三方库函数,要考虑到它们是否能在内部实现中有加锁/解锁操作(例如为了保证线程安全而加入的)。如果存在锁操作,则要考虑 “2.4节”中的情况(如果无法查看源码,则根据最不利原则,应该假定它们是存在锁操作的)