# 12.7.1 线程安全
当用线程编写程序时,必须小心地编写那些具有称为线程安全性 (thread safety) 属性的函数。一个函数被称为线程安全的 (thread-safe),当且仅当被多个并发线程反复地调用时,它会一直产生正确的结果。如果一个函数不是线程安全的,我们就说它是线程不安全的 (thread-unsafe)。
我们能够定义出四个(不相交的)线程不安全函数类:
第 1 类:不保护共享变量的函数。 我们在图 12-16 的 thread 函数中就已经遇到了这样的问题,该函数对一个未受保护的全局计数器变量加 1。将这类线程不安全函数变成线程安全的,相对而言比较容易:利用像 P 和 V 操作这样的同步操作来保护共享的变量。这个方法的优点是在调用程序中不需要做任何修改。缺点是同步操作将减慢程序的执行时间。
第 2 类:保持跨越多个调用的状态的函数。 一个伪随机数生成器是这类线程不安全函数的简单例子。请参考图 12-37 中的伪随机数生成器程序包。rand 函数是线程不安全的,因为当前调用的结果依赖于前次调用的中间结果。当调用 srand 为 rand 设置了一个种子后,我们从一个单线程中反复地调用 rand,能够预期得到一个可重复的随机数字序列。然而,如果多线程调用 rand 函数,这种假设就不再成立了。
code/conc/rand.c
unsigned next_seed = 1;
/* rand - return pseudorandom integer in the range 0..32767 */
unsigned rand(void)
{
next_seed = next_seed*1103515245 + 12543;
return (unsigned)(next_seed>>16) % 32768;
}
/* srand - set the initial seed for rand() */
void srand(unsigned new_seed)
{
next_seed = new_seed;
}
图 12-37 一个线程不安全的伪随机数生成器(基于 [61])
使得像 rand 这样的函数线程安全的唯一方式是重写它,使得它不再使用任何 static 数据,而是依靠调用者在参数中传递状态信息。这样做的缺点是,程序员现在还要被迫修改调用程序中的代码。在一个大的程序中,可能有成百上千个不同的调用位置,做这样的修改将是非常麻烦的,而且容易出错。
第 3 类:返回指向静态变量的指针的函数。 某些函数,例如 ctime 和 gethostbyname,将计算结果放在一个 static 变量中,然后返回一个指向这个变量的指针。如果我们从并发线程中调用这些函数,那么将可能发生灾难,因为正在被一个线程使用的结果会被另一个线程悄悄地覆盖了。
有两种方法来处理这类线程不安全函数。一种选择是重写函数,使得调用者传递存放结果的变量的地址。这就消除了所有共享数据,但是它要求程序员能够修改函数的源代码。
如果线程不安全函数是难以修改或不可能修改的(例如,代码非常复杂或是没有源代码可用),那么另外一种选择就是使用加锁-复制 (lock-and-copy) 技术。基本思想是将线程不安全函数与互斥锁联系起来。在每一个调用位置,对互斥锁加锁,调用线程不安全函数,将函数返回的结果复制到一个私有的内存位置,然后对互斥锁解锁。为了尽可能地减少对调用者的修改,你应该定义一个线程安全的包装函数,它执行加锁-复制,然后通过调用这个包装函数来取代所有对线程不安全函数的调用。例如,图 12-38 给出了 ctime 的一个线程安全的版本,利用的就是加锁-复制技术。
code/conc/ctime-ts.c
char *ctime_ts(const time_t *timep, char *privatep)
{
char *sharedp;
P(&mutex);
sharedp = ctime(timep);
strcpy(privatep, sharedp); /* Copy string from shared to private */
V(&mutex);
return privatep;
}
图 12-38 C 标准库函数 ctime 的线程安全的包装函数。使用加锁-复制技术调用一个第 3 类线程不安全函数
第 4 类:调用线程不安全函数的函数。 如果函数 f 调用线程不安全函数 g,那么 f 就是线程不安全的吗?不一定。如果 g 是第 2 类函数,即依赖于跨越多次调用的状态,那么 f 也是线程不安全的,而且除了重写 g 以外,没有什么办法。然而,如果 g 是第 1 类或者第 3 类函数,那么只要你用一个互斥锁保护调用位置和任何得到的共享数据,f 仍然可能是线程安全的。在图 12-38 中我们看到了一个这种情况很好的示例,其中我们使用加锁-复制编写了一个线程安全函数,它调用了一个线程不安全的函数。