桃源谷

心灵的旅行

人生就是一场旅行,不在乎旅行的目的地,在乎的是沿途的风景和看风景的心情 !
posts - 32, comments - 42, trackbacks - 0, articles - 0
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

准则6:遵守多线程编程的常识(上)

Posted on 2008-12-29 17:11 lymons 阅读(2549) 评论(0)  编辑 收藏 引用 所属分类: C++CUnix/Linux文章翻译
From 2008精选

UNIX上C++程序设计守则(6)Add star

准则6: 遵守多线程编程的常识


  1. 要准确把握在POSIX标准的函数中,那些函数是非线程安全的,一定不要使用
  2. 要让自己编写的函数符合线程安全
    • 在访问共享数据/变量之前一定要先锁定
    • 如果使用C++的话,一定要注意函数的同步方法

说明: (1) 要准确把握那些非线程安全的函数,一定不要使用


如果在POSIX平台上进行多线程编程时,有几个最基本的知识,也就是所说的“常识”,希望大家一定要严格遵守。


...首先、我们要理解“线程安全”的意思。线程安全的函数就是指,“一个能被在多个线程同时调用也不会发生问题的函数”。这样的函数通常要满足以下几个的特质。

  1. 不要操作局部的静态变量(函数内的static变量)和全局静态数据(全局变量,函数外的静态变量)。而且,也不要调用其他的非线程安全的函数
  2. 如果要操作这样的变量的话,事先必须使用互斥锁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;
}

这个函数如果被像下面那样使用的话,就会有漏洞:

  1. 在线程A里执行 ta = localtime(x);
  2. 在线程B里执行 tb = localtime(y);
  3. 线程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.

在多线程编程中,不要使用非线程安全的函数,而他们的备用函数可以放心地积极的去使用。


后续

*1:在C言語里函数不能重载,所以只能添加一个新的函数

*2:跟网络有关的API哪些是新的哪些是旧的,可以参考 IPv6网络编程 (network technology series) 这本好书


只有注册用户登录后才能发表评论。
【推荐】超50万行VC++源码: 大型组态工控、电力仿真CAD与GIS源码库
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


我的个人简历第一页 我的个人简历第二页