准则5: 尽可能避免线程中做延迟撤销的处理
- 线程的异步撤消是指:一个线程发出中断其他线程的处理的一个动作
- 延迟撤消因为是规格自由度比较高、所以根据OS和C库函数的版本它也有各式各样的动作
- 要想在不同的环境下都能稳定的动作的话,就必须要详细调查运行环境和,对C库函数进行抽象化,做必要的条件编译
- 在C++中、「撤消发生时的对象释放」的实现不具有可移植性
- 线程撤销要慎重使用。在C++里不要使用
说明:
在前面我们已经讲过,线程的撤消分为「异步」「延迟」这两种类型、并且「异步撤消」也是非常容易引起各种复杂问题的元凶。
那么,现在要在程序中除掉「延迟撤消」。延迟撤消虽然不会像异步撤消那样会引起各种各样的问题、但是、注意事项还是有很多的。只有把下面的这些注意事项全部都把握之后才能放心使用。
注意事项1: 要好好把握撤消点
和异步撤消不一样的是、撤消处理一直会被延迟到在代码上明示出来的撤消点之后才会被执行。如果编写了一个具有延迟撤消可能的代码、代码中的那条语句是撤消点、必须要正确的把握。
首先、调用过pthread_testcancel函数的地方就变成撤消点了。当然这个函数是、仅仅为了「变成延迟撤消」的目的而设置出来的函数。除此之外、某些标准库函数被调用后会不会变成撤消点是在规格(SUSv3)中决定的。请参照规格说明、有下面的函数一览。
下面的函数是撤消点
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,
(略)
看到这些我想已经明白了、但是在规格中也说明了「能否成为撤消点跟具体的实现相关的函数」也是多数存在的。原因是、为了可移植性、保证「在一定的时间内让线程的延迟撤消完成」是很困难的事情*1。做的不好的话、只要稍微一提升OS的版本就可能让做出来的程序产品不能动作。
即使是这样那还想要使用延迟撤消吗?
注意事项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); // 发生死锁! return 0; }
在上面的样例代码中、nanosleep执行的过程中经常会触发延迟撤销的最终动作,但是这个时候的mutex锁还处于被锁定的状态。而且、线程一被延迟撤消的话就意味着没有人去释放掉这个互斥锁了*2。因此、在下面的main函数中调用同样的cancel_unsafe函数时就会引起死锁了。
为了回避这个问题、利用pthread_cleanup_push函数在撤消时释放掉互斥锁的话就OK了,也就不会死锁了。
// 新增清除函数 void cleanup(void* mutex) { pthread_mutex_unlock((pthread_mutex_t*)mutex); } // 粗体字部分是新增的语句 void* cancel_unsafe(void*) { static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 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); return 0; }
注意事项3: 实现要清楚延迟撤消和C++之间的兼容度
使用C语言的场合,利用上面的pthread_cleanup_push/pop函数就能安全地执行延迟撤消的动作,但是在C++语言的场合就会出现其他的问题。C++与延迟撤消之间的兼容度是非常差的。具体的表现有以下两个问题:
- 执行延迟撤消的时候,内存栈上的对象的析构函数会不会被调用跟具体的开发环境有关系
- GCC3版本就不会调用。
- Solaris和Tru64 UNIX下的原生编译器的场合,就调用析构函数(好像)
- pthread_cleanup_push/pop函数和C++的异常处理机制之间有着怎样的相互影响也能具体环境有关
不调用析构函数,或者在抛出异常的时候不能做cleanup处理,经常是发生内存泄漏,资源丢失,程序崩溃,死锁等现象的原因。令人意外的是对于这个深层次的问题,就连Boost 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++的工程里不对线程进行延迟撤消处理还是比较实际的。
*1:好的问题是 gethostbyname()函数
*2:异步撤消跟malloc函数的例子很相似
原文地址:http://d.hatena.ne.jp/yupo5656/20040725/p2