准则6: 遵守多线程编程的常识
- 要准确把握在POSIX标准的函数中,那些函数是非线程安全的,一定不要使用
- 要让自己编写的函数符合线程安全
- 在访问共享数据/变量之前一定要先锁定
- 如果使用C++的话,一定要注意函数的同步方法
说明: (1) 要准确把握那些非线程安全的函数,一定不要使用
如果在POSIX平台上进行多线程编程时,有几个最基本的知识,也就是所说的“常识”,希望大家一定要严格遵守。
...首先、我们要理解“线程安全”的意思。线程安全的函数就是指,“一个能被在多个线程同时调用也不会发生问题的函数”。这样的函数通常要满足以下几个的特质。
- 不要操作局部的静态变量(函数内的static变量)和全局静态数据(全局变量,函数外的静态变量)。而且,也不要调用其他的非线程安全的函数
- 如果要操作这样的变量的话,事先必须使用互斥锁mutex进行同步,否则一定要限制多个线程同时对它的访问
那么、在POSIX标准的函数里面,也有不满足上述条件的。由于历史遗留问题,一些函数的识别标识(signature)的定义没有考虑到线程安全的问题,所以不管怎么做都不能满足上述的条件。例如,看看 localtime函数吧。它的定义的识别标识(signature)如下:
struct tm *localtime(const time_t *timer);
localtime 函数是,把一个用整数形式表示的时刻(从1970/1/1到现在为止的秒数)、转换成一个能让人容易明白的年月日形式表示出来的tm结构体并返回给调用者 的函数。根据规格说明、返回出来的tm结构体是不需要free()掉,也不能释放的。这个函数典型的实现就像下面的代码那样:
struct tm *localtime(const time_t *timer) {
static struct tm t;
/* ... 从timer参数里算出年月日等数值 ... */
t.tm_year = XXX;
/* ...把它们填入到结构体内... */
t.tm_hour = XXX;
t.tm_min = XXX;
t.tm_sec = XXX;
return &t;
}
这个函数如果被像下面那样使用的话,就会有漏洞:
- 在线程A里执行 ta = localtime(x);
- 在线程B里执行 tb = localtime(y);
- 线程A参照ta结构体里的数据 → 就发现这些数据是一些奇怪的值!
...在函数的说明手册里对这个问题也没有做过详细的说明。关于这个漏洞,在localtime函数即使使用了mutex锁也不能被回避掉。所以,这个函数定义的识别标识是不行滴。
[译 者lymons注:在多个线程里调用localtime函数之所以有问题的原因是,localtime函数里返回的tm构造体是一个静态的结构体,所以在 线程A里调用localtime函数时,该结构体被赋予正确的值;而在线程A参照这个结构体之前,线程B又调用localtime的话,这个静态的结构体 又被赋予新的一个值。因此在线程A对这个结构体的访问都是基于一个错误的值进行的]
正因为如此,就像上面说过的POSIX规格(SUSv3)里整齐的定义了一些“非线程安全的函数”。在"§2.9.1 Thread-Safety" 这里登载了的非线程安全的函数有如下所示。
asctime, basename, catgets, crypt, ctime, dbm_clearerr, dbm_close, dbm_delete, dbm_error, dbm_fetch, dbm_firstkey, dbm_nextkey, dbm_open, dbm_store, dirname, dlerror, drand48, ecvt, encrypt, endgrent, endpwent, endutxent, fcvt, ftw, gcvt, getc_unlocked, getchar_unlocked, getdate, getenv, getgrent, getgrgid, getgrnam,
(省略)
对于在规格中被定义为非线程安全的函数,应该制定一个避免使用它们的规则出来,并且制作一个能够自动检查出是否使用了这些函数的开发环境,应该是比较好的。
反之,在这里没有被登载的POSIX标准函数都被假定为 "shall be thread-safe" 的、所以在实际的使用中可以认为在多线程环境里是没有问题的(而且在使用的平台上没有特别地说明它是非线程安全的话)。
另外,有几个非线程安全的函数,都准备了一个备用的线程安全版本的函数(仅仅是变更了函数的识别标识)。像这些函数为了与原版进行区别都在其函数名后面添加了 _r 这个后缀*1。例如,asctime函数就有线程安全版本的函数asctime_r。在规格说明中是否定义了备用函数,可以试着点击刚才的那个网页里面的函数名就可以看到。点击 rand函数就可以看到,
[TSF] int rand_r(unsigned *seed);
用[TSF]这样的文字标记出来的函数吧。这就是备用函数。在一览中没有记载出来的函数(备注: 稍微有点儿出入。请参照这里)、据我所知还有下面的备用函数。
asctime_r, ctime_r, getgrgid_r, getgrnam_r, getpwnam_r, getpwuid_r, gmtime_r, localtime_r, rand_r, readdir_r, strerror_r, strtok_r
还有,在规格以外,还准备了很多的下面那样的函数。
gethostbyname_r, gethostbyname2_r
在最近的操作系统中、也使用 getaddrinfo API函数来解决IPv6名字对应的问题。gethostbyname系列的API都是比较陈旧的函数了、所以使用前面的函数还是比较好吧*2。根据规格SUSv3,getaddrinfo也是线程安全的:
The freeaddrinfo() and getaddrinfo() functions shall be thread-safe.
在多线程编程中,不要使用非线程安全的函数,而他们的备用函数可以放心地积极的去使用。