Linux/Unix 编程中 POSIX 函数的线程安全问题

线程安全介绍

在目前的计算机科学中,线程是操作系统调度的最小单元,进程是资源分配的最小单元。在大多数操作系统中,一个进程可以同时派生出多个线程。这些线程独立执行,共享进程的资源。在单处理器系统中,多线程通过分时复用技术来技术,处理器在不同的线程间切换,从而更高效地利用系统 CPU资源。在多处理器和多核系统中,线程实际上可以同时运行,每个处理器或者核可以运行一个线程,系统的运算能力相对于单线程或者单进程大幅增强。

多线程技术让多个处理器机器,多核机器和集群系统运行更快。因为多线程模型与生俱来的优势可以使这些机器或者系统实现真实地的并发执行。但多线程在带来便利的同时,也引入一些问题。线程主要由控制流程和资源使用两部分构成,因此一个不得不面对的问题就是对共享资源的访问。为了确保资源得到正确的使用,开发人员在设计编写程序时需要考虑避免竞争条件和死锁,需要更多地考虑使用线程互斥变量。

线程安全 (Thread-safe) 的函数就是一个在代码层面解决上述问题比较好的方法,也成为多线程编程中的一个关键技术。如果在多线程并发执行的情况下,一个函数可以安全地被多个线程并发调用,可以说这个函数是线程安全的。反之,则称之为“非线程安全”函数。注意:在单线程环境下,没有“线程安全”和“非线程安全”的概念。因此,一个线程安全的函数允许任意地被任意的线程调用,程序开发人员可以把主要的精力在自己的程序逻辑上,在调用时不需要考虑锁和资源访问控制,这在很大程度上会降低软件的死锁故障和资源并发访问冲突的机率。所以,开发人员应尽可能编写和调用线程安全函数。

如何编写线程安全函数

判断一个函数是否线程安全不是一件很容易的事情。但是读者可以通过下面这几条确定一个函数是线程不安全的。

  • a, 函数中访问全局变量和堆。
  • b, 函数中分配,重新分配释放全局资源。
  • c, 函数中通过句柄和指针的不直接访问。
  • d, 函数中使用了其他线程不安全的函数或者变量。

因此在编写线程安全函数时,要注意两点:

  • 1, 减少对临界资源的依赖,尽量避免访问全局变量,静态变量或其它共享资源,如果必须要使用共享资源,所有使用到的地方必须要进行互斥锁 (Mutex) 保护;
  • 2, 线程安全的函数所调用到的函数也应该是线程安全的,如果所调用的函数不是线程安全的,那么这些函数也必须被互斥锁 (Mutex) 保护;

举个例子(参考 例子 1),下面的这个函数 sum()是线程安全的,因为函数不依赖任何全局变量。


例子 1
int sum(int i, int j) { 
      return (i+j); 
 }

但如果按下面的方法修改,sum()就不再是线程安全的,因为它调用的函数 inc_sum_counter()不是线程安全的,该函数访问了未加锁保护的全局变量 sum_invoke_counter。这样的代码在单线程环境下不会有任何问题,但如果调用者是在多线程环境中,因为 sum()有可能被并发调用,所以全局变量 sum_invoke_counter很有可能被并发修改,从而导致计数出错。


例子 2
static int sum_invoke_counter = 0; 

 void inc_sum_counter(int i, int j) { 
     sum_invoke_counter++; 
 } 
   
 int sum(int i, int j) { 
    inc_sum_counter(); 
    return (i+j); 
 }

我们可通过对全局变量 sum_invoke_counter添加锁保护,使得 inc_sum_counter()成为一个线程安全的函数。


例子 3
static int sum_invoke_counter = 0; 
 static pthread_mutex_t sum_invoke_counter_lock = PTHREAD_MUTEX_INITIALIZER; 

 void inc_sum_counter(int i, int j) { 
    pthread_mutex_lock( &sum_invoke_counter_lock ); 
    sum_invoke_counter++; 
    pthread_mutex_unlock( &sum_invoke_counter_lock ); 
 } 
   
 int sum(int i, int j) { 
    inc_sum_counter(); 
    return (i+j); 
 }

现在 , sum()inc_sum_counter()都成为了线程安全函数。在多线程环境下,sum()可以被并发的调用,但所有访问 inc_sum_counter()线程都会在互斥锁 sum_invoke_counter_lock上排队,任何一个时刻都只允许一个线程修改 sum_invoke_counter,所以 inc_sum_counter()就是现成安全的。

除了线程安全还有一个很重要的概念就是 可重入(Re-entrant),所谓可重入,即:当一个函数在被一个线程调用时,可以允许被其他线程再调用。显而易见,如果一个函数是可重入的,那么它肯定是线程安全的。但反之未然,一个函数是线程安全的,却未必是可重入的。程序开发人员应该尽量编写可重入的函数。

一个函数想要成为可重入的函数,必须满足下列要求:

  • a) 不能使用静态或者全局的非常量数据
  • b) 不能够返回地址给静态或者全局的非常量数据
  • c) 函数使用的数据由调用者提供
  • d) 不能够依赖于单一资源的锁
  • e) 不能够调用非可重入的函数

对比前面的要求,例子 1sum()函数是可重入的,因此也是线程安全的。例子 3中的 inc_sum_counter()函数虽然是线程安全的,但是由于使用了静态变量和锁,所以它是不可重入的。因为 例子 3中的 sum()使用了不可重入函数 inc_sum_counter(), 它也是不可重入的。

相关推荐