天天看点

UNIX上C++程序设计守则(信号和线程)(下)

线程的异步撤销是指:某个线程的执行立刻被其他线程给强制终止了

请不要单单为了让“设计更简单”或者“看起了更简单”而使用线程的异步撤消

咋一看还是挺简单的。但是搞不好可能会引起各种各样的问题。请不要在不能把握问题的实质就做出使用线程的异步撤消的设计!

在pthread的规格说明中,允许一个线程可以强制中断某个线程的执行。这就是所说的异步撤消。

线程的撤消有下面的两种方式。

方式1: 异步撤消(PTHREAD_CANCEL_ASYNCHRONOUS)

撤销动作是马上进行的

方式2: 延迟撤销(PTHREAD_CANCEL_DEFERRED) (默认设置)

撤消动作,是让线程的处理一直被延迟到撤消点才会去执行

会造成什么问题呢

      那么,让我看看乱用线程的异步撤消会引起什么问题呢。看过准则3的人可能会知道,在下面的脚本里,被撤销线程以外的任意一个线程会被死锁。

1.   线程1中调用malloc函数正在做内存分配的过程中,线程2异步撤消了线程1的处理

2.   线程1马上被撤销,但是malloc函数中的互斥锁就没有线程去解除了

3.   后面的任意一个线程如果再次调用malloc函数的话就会马上导致该线程死锁

      在这个例子中使用了malloc函数,但是其他的危险函数还有很多。

pthread_cancel

pthread_setcancelstate

pthread_setcanceltype

      而且,里面还有"No other functionsare required to be async-cancel-safe"这样的记载。因此,Linux的场合,如果在文档里没有记载成async-cancel safety的函数,我们还是把它假定成不安全的函数为好!

如何避免这些问题呢

     在多线程编程中为了安全的使用异步撤消处理、有没有回避死锁的方法呢?我们试着想了几个。他们与准则3里的线程+fork的场合的回避策很相似。

回避方法1: 被撤销线程中,只能使用异步撤消安全函数

首先,被撤销线程中,只能使用异步撤消安全函数。但是这个方法

在规格说明中只有3个异步撤消安全的函数

这些以外的函数是不是异步撤消安全(商用UNIX)、因为没有说明文档我们不清楚(Linux)

中有以上的两点,所以这个回避方法几乎不现实。

回避方法2: 被撤销线程中,在做非异步撤消安全处理的过程中,先把撤消方式设置成「延迟」或者是「禁止」

第二个是,被撤销线程在做非异步撤消安全处理的过程中,把撤消方式再设定成「延迟」或者「禁止」。对于这个方法

就像方法1写的那样、要把我那个函数是异步撤消安全的一时还是挺麻烦的

在任意的场所并不能保证撤消动作会被马上执行

例如,再设定成「延迟」后的一段时间内如果撤消发生时、某个正在阻塞的I/O函数是否能够被解除阻塞还是挺微妙的

如果设定成撤消禁止的话,则撤消会被屏蔽掉

有上面样的问题、会导致「一精心设计撤消方式的替换,从一开始就使用延迟撤消还不够好」这样的结果。所以这几乎是不好的一个回避策。

回避方法3: 使用pthread_cleanup_push函数,登录异步撤消时的线程数据清除的回调函数

第三种则是,用pthread_cleanup_push函数、登录一个在异步撤消发生时的数据清除的回调函数。这和在准则3中介绍的pthread_atfork函数有点儿类似。用这个函数登录的回调函数来清除线程的数据和锁,就可以回避死锁了。

回避方法4: 不要执行异步撤消处理

最后是、不要执行异步撤消处理。反而代之的是、

设计成不依赖使用异步撤消那样的处理

不得不使用线程撤消的话,不做异步撤消而作延迟撤消的处理

这是比较实际的做法,是我们值得推荐的。

线程的异步撤消是指:一个线程发出中断其他线程的处理的一个动作

延迟撤消因为是规格自由度比较高,所以根据OS和C库函数的版本它也有各式各样的动作

要想在不同的环境下都能稳定的动作的话,就必须要详细调查运行环境和对C库函数进行抽象化,做必要的条件编译

在C++中,「撤消发生时的对象释放」的实现不具有可移植性

线程撤销要慎重使用。在C++里不要使用

说明:

在前面我们已经讲过,线程的撤消分为「异步」「延迟」这两种类型,并且「异步撤消」也是非常容易引起各种复杂问题的元凶。

那么,现在要在程序中除掉「延迟撤消」。延迟撤消虽然不会像异步撤消那样会引起各种各样的问题、但是,注意事项还是有很多的。只有把下面的这些注意事项全部都把握之后才能放心使用。

注意事项1: 要好好把握撤消点

      和异步撤消不一样的是:撤消处理一直会被延迟到在代码上明示出来的撤消点之后才会被执行。如果编写了一个具有延迟撤消可能的代码,代码中的那条语句是撤消点,必须要正确的把握。

下面的函数是撤消点

accept, aio_suspend, clock_nanosleep, close, connect,creat, fcntl, fdatasync,fsync, getmsg, getpmsg, lockf, mq_receive, mq_send,mq_timedreceive,mq_timedsend, msgrcv, msgsnd, msync, nanosleep, open, pause,poll, pread,pselect, pthread_cond_timedwait,pthread_cond_wait, pthread_join,pthread_testcancel, putmsg, putpmsg,pwrite, read, readv, recv, recvfrom,(略)

下面的函数不是撤消点

access, asctime, asctime_r, catclose, catgets, catopen,closedir, closelog,ctermid, ctime, ctime_r, dbm_close, dbm_delete, dbm_fetch,dbm_nextkey, dbm_open,dbm_store, dlclose, dlopen, endgrent, endhostent,endnetent, endprotoent,endpwent, endservent, endutxent, fclose, fcntl, fflush,fgetc, fgetpos, fgets,fgetwc, fgetws, fmtmsg, fopen, fpathconf, fprintf, fputc,fputs, fputwc, fputws,(略)

即使是这样那还想要使用延迟撤消吗?

注意事项2: 实现要知道cleanup函数的必要性

      可能被延迟撤销的线程在运行的过程中,要申请资源的场合,一定要考虑到以下的几点,否则就会编制出含有资源丢失和死锁的软件产品。

例如编写的下面的函数就不能被安全的延迟撤销掉。

void* cancel_unsafe(void*) {

    static pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER;

    pthread_mutex_lock(&mutex);                        // 此处不是撤消点

    struct timespec ts = {3, 0};nanosleep(&ts, 0); // 经常是撤消点

    pthread_mutex_unlock(&mutex);                    // 此处不是撤消点

    return 0;

}

int main(void) {

    pthread_t t;

    // pthread_create后马发上收到一个有效的延迟撤消的要求

    pthread_create(&t, 0,cancel_unsafe, 0);

    pthread_cancel(t);

    pthread_join(t, 0);

    cancel_unsafe(0); // 发生死锁!

      为了回避这个问题,利用pthread_cleanup_push函数在撤消时释放掉互斥锁的话就OK了,也就不会死锁了。

// 新增清除函数

void cleanup(void* mutex) { 

   pthread_mutex_unlock((pthread_mutex_t*)mutex);

// 粗体字部分是新增的语句

    pthread_cleanup_push(cleanup,&mutex);

    pthread_mutex_lock(&mutex);

    struct timespec ts = {3, 0};nanosleep(&ts, 0);

    pthread_mutex_unlock(&mutex);

    pthread_cleanup_pop(0);

注意事项3: 实现要清楚延迟撤消和C++之间的兼容度

      使用C语言的场合,利用上面的pthread_cleanup_push/pop函数就能安全地执行延迟撤消的动作,但是在C++语言的场合就会出现其他的问题。C++与延迟撤消之间的兼容度是非常差的。具体的表现有以下两个问题:

执行延迟撤消的时候,内存栈上的对象的析构函数会不会被调用跟具体的开发环境有关系

GCC3版本就不会调用。

Solaris和Tru64 UNIX下的原生编译器的场合,就调用析构函数(好像)

pthread_cleanup_push/pop函数和C++的异常处理机制之间有着怎样的相互影响也能具体环境有关

[Q] Why isn't thread cancellation or termination provided?

[A] There's a valid need for thread termination, so at some point Boost.Threads probably will include it, but only after we can find a truly safe(and portable) mechanism for this concept.

先必须确保对象的自由存储,而后全都让cleanup函数去释放对象的方法也有,但是这次是牺牲了异常安全性。

应该说的是,在使用C++的工程里不对线程进行延迟撤消处理还是比较实际的。

要准确把握在POSIX标准的函数中,那些函数是非线程安全的,一定不要使用

要让自己编写的函数符合线程安全

在访问共享数据/变量之前一定要先锁定

如果使用C++的话,一定要注意函数的同步方法

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

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

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

不要操作局部的静态变量(函数内的static变量)和全局静态数据(全局变量,函数外的静态变量)。而且,也不要调用其他的非线程安全的函数

如果要操作这样的变量的话,事先必须使用互斥锁mutex进行同步,否则一定要限制多个线程同时对它的访问

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对这个结构体的访问都是基于一个错误的值进行的]

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" 的、所以在实际的使用中可以认为在多线程环境里是没有问题的(而且在使用的平台上没有特别地说明它是非线程安全的话)。

[TSF] int rand_r(unsigned *seed);

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

The freeaddrinfo() and getaddrinfo() functions shall bethread-safe.

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

<a href="http://d.hatena.ne.jp/yupo5656/20040809/p2" target="_blank">后续</a>

(2)要让自己编写的函数符合线程安全

      在写多线程的应用程序时,在多个线程里共享的变量要先锁定然后在更新它。那么在多线程里共享的变量主要有全局变量和函数内的静态变量。而且,即使是short型和int型的共享变量也要先锁定后更新才能保证其安全。

      还有,在使用C++编程的场合要注意函数的方步方法。一般的说来下面的写法是错误的。Mutex在函数内被声明成静态变量是不允许的。

int incr_counter(void) {

  static Mutex m;  // 这么写不行

  m.Lock();

  static int counter = 0;

  int ret = ++counter;

  m.Unlock();

  return ret;

应该用下面的方式来代替,

Mutex m;

  // ...

把Mutex声明成全局变量的话比较好(稍微比上一个好)。

UNIX上C++程序设计守则(6)-- 补记

线程安全函数是像下面那样

不要操作局部的静态变量(函数内的static型的变量)和非局部的静态数据(全局变量)。并且,其它的非线程安全函数不要调用

要操作这样的变量的话, 就要使用mutex进行同步处理,来限制多个线程同时对它进行操作

被定义的,但是

特别是前者, 和被叫做可重入的(reentrant)函数有区别

反之, 后者特别是和叫做"Serializable"(不单单是MT-Safe)"Safe"的函数有区别

嗯, 因为比较详细的, 如果不是在对于执行速度要求比较苛刻的环境中编写代码的话, 单单地意识到「是否线程安全」就足够了,不是吗。

---------------------------------------------------

欢迎转载,请注明作者和出处

本文转自 zhenjing 博客园博客,原文链接:http://www.cnblogs.com/zhenjing/archive/2010/12/23/thread_cancel.html   ,如需转载请自行联系原作者