6.7.1 终止线程(2)
在示例6-3中,取消被设置为立即发生。这意味着取消线程的请求可以在线程的函数执行中的任何一点发生。这样,线程可以打开文件并在对文件进行写入时被取消。
取消对等线程不应当轻易进行。有些线程具有非常敏感的性质,可能会要求安全保护以防止不合时宜的取消。在线程的函数中安装安全保护能够防止不期望的状况发生。例如,考虑共享数据的线程。根据使用的线程模型,一个线程可能正在处理数据,这些数据要传递给另一个线程进行处理。当线程处理数据时,它通过对互斥量的加锁获得对数据的独自占有。如果线程在互斥量释放之前被取消,这将会导致死锁。在数据能够再次被使用之前,可能会要求它处于某种状态。如果线程在完成这些之前被取消,会发生不期望的状况。根据线程正在进行的处理的类型,线程取消只应当在安全的时候进行。
重要的线程可能会完全防止取消。因此,线程取消应当被限制在不重要的线程上,而且是没有对资源加锁或者没有在执行重要代码的执行点上。您应当将线程的可取消性设置为适当的状态和类型。取消应当被延迟,直到所有重要的清理都发生了,例如释放互斥量、关闭文件等。如果线程有着取消清理处理程序任务,它们应当在取消之前进行。当返回最后的处理程序后,调用线程特有数据的析构器,然后线程被终止。
使用取消点
当推迟取消请求时,线程的终止会推迟到线程的函数执行的后期。当取消发生时,应当是安全的,因为它不处于对互斥量加锁、执行关键代码、令数据处于某种不可用状态的情况中。代码的执行中,这些安全的位置是很好的取消点位置。取消点是一个检查点,线程在这里检查是否有任何取消请求未决,如果有,则终止。
取消点通过调用pthread_testcancel( )来标记。这个函数用于检查任意的未决取消请求。如果有未决的请求,它会导致取消过程发生在这个函数被调用的位置。如果没有未决的取消请求,则函数继续执行,调用不产生任何影响。这个函数调用应当放置在代码中认为可以安全终止进程的任意位置。
调用形式
- #include <pthread.h>
-
- void pthread_testcancel(void);
在示例6-3中,线程的可取消性被设置为立即可取消。示例6-4使用延迟取消,这是默认的设置。对pthread_testcancel( )的调用标记了在哪里取消这个线程是安全的,即在文件被打开之前,或线程关闭文件之后。
示例6-4
- // Example 6-4 task1 thread sets its cancelability state to be deferred.
-
-
- void *task1(void *X)
- {
- int OldState,OldType;
-
- //not needed default settings for cancelability
- pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,&OldState);
- pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED,&OldType);
-
- pthread_testcancel();
-
- ofstream Outfile("out1.txt");
- for(int Count = 1;Count < 1000;Count++)
- {
- Outfile << "thread 1 is working: " << Count << endl;
-
- }
- Outfile.close();
- pthread_testcancel();return (NULL);
- }
在示例6-5中,我们创建了两个线程并将它们取消。
示例6-5
- //Example 6-5 shows two threads being canceled.
-
- //...
- int main(int argc, char *argv[])
- {
- pthread_t Threads[2];
- void *Status;
-
- pthread_create(&(Threads[0]),NULL,task1,NULL);
- pthread_create(&(Threads[1]),NULL,task3,NULL);
-
-
- // ...
-
- pthread_cancel(Threads[0]);
- pthread_cancel(Threads[1]);
-
-
- for(int Count = 0;Count < 2;Count++)
- {
- pthread_join(Threads[Count],&Status);
- if(Status == PTHREAD_CANCELED){
- cout << "thread" << Count << " has been canceled" << endl;
- }
- else{
- cout << "thread" << Count << " has survived" << endl;
- }
- }
- return (0);
- }
在示例6-5中,主线程创建两个线程,然后对每个线程发出了取消请求。主线程为每个线程调用了pthread_join( )。函数pthread_join( )如果试图同已经被终止的线程进行结合,则不会失败。结合函数提取被终止线程的退出状态。这种方式较好,因为发出取消请求的线程可能不是调用pthread_join( )的线程。监视所有worker线程的工作情况可能是某个线程的主要任务,该线程也负责取消线程。另一个线程可能会通过调用pthread_join( )函数来检测线程们的退出状态。这种信息可用于静态评估哪个线程的性能最好。在本例中,主线程在循环中结合并检查每个线程的退出状态。被取消的线程会返回退出状态PTHREAD_CANCELED。
利用可安全取消的库函数和系统调用
在这些例子中,通过调用pthread_testcancel( )标记的取消点放置在用户定义的函数中。当您从使用异步取消的线程函数中调用库函数时,取消这些线程是安全的吗?
pthread库定义了可作为取消点的函数以及认为可异步安全取消的函数。这些函数阻塞调用线程,当调用线程被阻塞时,取消线程是安全的。这些是用作取消点的pthread库函数:
- pthread_testcancel( )
-
- pthread_cond_wait( )
-
- pthread_timedwait( )
-
- pthread_join( )
如果状态为延迟取消的线程在做出对这些pthread库函数的调用时,有挂起的取消请求,则开始进行取消过程。
表6-6列出了一些被要求为取消点的POSIX系统调用。这些pthread和POSIX函数可以安全地用作延迟取消点,但是可能对于异步取消是不安全的。如果不是可安全异步取消的库调用在执行期间被取消,则可能导致库数据处于矛盾的状态。库可能已经为线程分配了内存,当线程被取消时,可能仍占有该内存。在这种情况下,当从不是可异步取消的线程进行这样的库调用时,有必要在调用之前改变可取消状态,并在函数返回后将可取消状态改变回来。
表6-6
|
POSIX系统调用(取消点)< xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" /> |
|
accept( ) |
nanosleep( ) |
sem_wait( ) |
|
aio_suspend( ) |
open( ) |
send( ) |
|
clock_nanosleep( ) |
pause( ) |
sendmsg( ) |
|
close( ) |
poll( ) |
sendto( ) |
|
connect( ) |
pread( ) |
sigpause( ) |
|
create( ) |
pthread_
cond_timedwait( ) |
sigsuspend( ) |
|
fcntl( ) |
pthread_cond_wait( ) |
sigtimedwait( ) |
|
fsync( ) |
pthread_join( ) |
sigwait( ) |
|
getmsg( ) |
putmsg( ) |
sigwaitinfo( ) |
|
lockf( ) |
putpmsg( ) |
sleep( ) |
|
mq_receive( ) |
pwrite( ) |
system( ) |
|
mq_send( ) |
read( ) |
usleep( ) |
|
mq_timedreceive( ) |
readv( ) |
wait( ) |
|
mq_timedsend( ) |
recvfrom( ) |
waitpid( ) |
|
msgrcv( ) |
recvmsg( ) |
write( ) |
|
msgsnd( ) |
select( ) |
writev( ) |
|
msync( ) |
sem_timedwait( ) |
|
对于其他不是可安全取消(异步或延迟)的库函数和系统函数,可能需要书写代码,通过禁止取消或将取消延迟到函数已经返回来防止线程终止。
示例6-6是对库调用或系统调用的封装。通过封装将可取消性改为延迟的、进行函数或系统调用,然后将可取消性重设回之前的类型。现在就可以安全地调用pthread_testcancel( )了。
示例6-6
- //Example 6-6 shows a wrapper for system functions.
-
- int OldType;
- pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED,&OldType);
- system_call(); //some library of system call
- pthread_setcanceltype(OldType,NULL);
- pthread_testcancel();
-
- //...
终止之前进行清理
我们在前面提到过,线程在终止之前,可能需要执行一些最终的处理,例如关闭文件、将共享资源重设为一致的状态、释放锁和释放资源。pthread库定义了一种机制,来为每个线程在终止之前执行最后的任务。清理栈(cleanup stack)同每个线程关联,在清理栈中包含了指向取消过程中要执行的例程的指针。函数pthread_cleanup_push( )将一个指向例程的指针压入清理栈中。
调用形式
- #include <pthread.h>
-
- void pthread_cleanup_push(void (*routine)(void *), void *arg);
- void pthread_cleanup_pop(int execute);
参数routine是要被压入栈中的函数指针,参数arg被传递给该函数。当线程在这些环境下退出时,会以arg为参数调用函数routine:
当调用pthread_exit( )时
当线程接受终止请求时
当线程使用非零execute值显式调用pthread_cleanup_pop( )时
该函数不进行返回。
函数pthread_cleanup_pop( )从调用线程的清理栈的顶部删除routine的指针。参数execute的值可以为1或0。如果为1,线程执行routine,即使它没有被终止。线程从调用该函数之后的位置继续执行。如果值为0,则指针会从栈顶移除,指向的函数不会被执行。
对于每次入栈,需要在相同的词法范围(lexical scope)中存在出栈。例如,task4( )要求函数退出或被取消时执行清理处理程序。
在示例6-7中,task4( )通过调用pthread_cleanup_push( )函数将清理处理程序cleanup_task4( )压入栈中。对于每个对pthread_cleanup_push( )函数的调用,都要求有相应的pthread_cleanup_pop( )。pop函数被传递参数0,意味着此时处理程序已经从清理栈中移出,但是此时尚未被执行。如果取消了执行task4( )的线程,则会执行该处理程序。
示例6-7
- //Example 6-7 task4 () pushes cleanup handler
cleanup_task4 () onto cleanup stack. -
- void *task4(void *X)
- {
- int *Tid;
- Tid = new int;
- // do some work
- //...
- pthread_cleanup_push(cleanup_task4,Tid);
- // do some more work
- //...
- pthread_cleanup_pop(0);
- }
在示例6-8中,task5( )将清理处理程序cleanup_task5( )压入到清理栈中。这个例子同上一个例子的区别在于传递给pthread_cleanup_pop( )的参数为1,意味着处理程序从清理栈中移出,并在这个位置被执行。无论执行task5( )的线程是否被取消,处理程序都会被执行。清理处理程序cleanup_task4( )和cleanup_task5( )都是正规的函数,可用于关闭文件、释放资源、解锁互斥量等。
示例6-8
- //Example 6-8 task5 () pushes cleanup handler
cleanup_task5 () onto cleanup stack. -
- void *task5(void *X)
- {
- int *Tid;
- Tid = new int;
- // do some work
- //...
- pthread_cleanup_push(cleanup_task5,Tid);
- // do some more work
- //...
- pthread_cleanup_pop(1);
- }