第32章 线程:线程取消

的        在通常情况下,程序中的多个线程会并发执行,每个线程各司其职,直至其决议推出,随即会调用函数pthread_exit()或者从线程其中函数中返回。

        有时候,需要将一个线程取消(cancel).亦即,像一个线程发送一个请求,要求立即退出。比如一组线程正在执行一个运算,一旦某个线程检测到错误发生,需要其他线程退出,取消线程的功能这时就派上用场。还有一个情况,一个由图形用户界面(GUI)驱动的应用程序可能会提供一个取消按钮,以便用户可以终止后台某一线程正在执行的任务。这种情况下,主线程(控制图形用户界面)需要请求后台线程退出。

本章讨论POSIX线程的取消机制

32.1 取消一个线程

        函数pthread_cancel()向由thread指定的线程发送一个取消请求。

#include <pthread.h>
int pthread_cancel(pthread_t thread);
        Returns 0 on success, or a positive error number on error

        发出取消请求后,函数pthread_cancel()当即返回,不会等待目标线程的而退出。

        准确的说,目标线程会发生什么,何时发生?这些都取决于下节将要述及的线程取消状态和类型

32.3 取消状态及类型

        函数pthread_setcncelstate()和pthread_setcnceltype()会设定标志,允许线程对取消请求的响应过程加以控制。

#include <pthread.h>

int pthread_setcncelstate(int state,int *oldstate);
int pthread_setcnceltype(int type, int *oldtype);

      Both return 0 on success,or a positive wrror number on error

        函数pthread_setcancelstate()会调用线程的取消性状态置为参数state所给定的值。该参数的值如下。

PTHREAD_CANCEL_DISABLE

        线程不可取消,如果此类线程收到取消请求,则会将请求挂起

,直至将线程的取消状态置为启用。

PTHREAD_CANCEL_ENABLE

        线程可以取消,这是新建线程取消性状态的默认值。

        线程的前一取消状态将返回至参数oldstate所指向的位置

        如果对迁移状态没有兴趣,Linux允许将oldstate置为NULL.在很多其他的系统实现中,情况也是如此。不过,SUSv3并没有规范这一特性,所以要保证应用的可移植性,就不能以来这一特性。应该总是为oldstate设置一个非NULL的值。

        如果线程执行的代码片段需要不间断地一气呵成,那么临时屏蔽线程地取消性状态(PTHREAD_CANCEL_DISABLE)就变得很有必要。

        如果线程地取消性状态为“启用”(PTHREAD_CANCEL_ENABLE),那么对取消请求地处理则取决于线程地取消性类型,该类型可以通过调用函数pthread_setcnceltype()时地参数type给定。参数type有如下值:

PTHREAD_CANCEL_ASYNCHRONOUS

        可能会在任何时间点(也许是立即取消,但不一定)取消线程。异步取消地应用场景很少,32.6节会讨论

PTHREAD_CANCEL_DEFERED

        取消请求保持挂起状态,直至到达取消点(caccellation point)。这也是新建线程地缺省类型,后续将接受延迟取消地更多细节。

        线程原有地取消类型将返回至参数oldtyp所指向地位置。

与函数pthread_setcancelstate()地参数oldstate类似,如果不关心原有取消类型,许多系统实现包括Linux允许将oldtype置为NULL。同样,SUSv3也没有规范这一行为,所以需要保障可移植性地应用不应使用这一特性,应该总是为oldtype设置一个非NULL值

        当线程调用fork()时,子进程会继续调用线程地取消性类型及状态。而当某线程调用exec()时,会将新程序主线程地取消类型及状态分别重置为PTHREAD_CANCEL_ENABLE和PTHREAD_CANCEL_DEFERED

32.3 取消点

        若将线程地取消性状态和类型分别置为启用和延迟,仅当线程抵达某个取消点时,取消请求才会起作用,取消点即是由实现定义地一组函数之一加以调用

        SUSv3规定,若实现提供了表32-1中所列地函数,则这些函数必须是取消点。其中地大部分函数都有能力将线程无限期地堵塞起来。

         除表32-1所列函数之外,SUSv3还指定了大量函数,系统实现可以将其定义为取消点。其中包括stdio函数、dlopen API,syslog API、ntfw()、popen()、semop()、unlink,以及从诸如utmp之类地系统文件中获取信息地各种函数。可移植应用程序必须争取处理这一情况:线程在调用这邪恶函数时有可能遭到取消。

        SUSv3规定,除了上述两组必须或可能是可取消点地函数之外,不得将标准中地任何其他函数视为取消点(亦即,调用这些函数不会招致线程需求小,可移植程序无需加以处理)。

        SUSv4在必须地可取消点函数列表中增加了openat(),并移除了函数sigpause()(将其移至可能地取消点函数列表中)和函数usleep()(已从标准中删除)。

系统实现可随意将标准并未规范地其他函数标记为取消点。任何可能造成堵塞地函数(有可能是需要访问文件)都是取消点地理想候选对象,出于这个理由glibc将其中地许多非标准函数标记为取消点。

        线程一旦收到取消请求,且启用了取消性状态并将类型置为延迟,则其会在下次抵达取消点时终止。如果该线程尚未分离,那么为防止其变为僵尸进程,必须由其他线程对其进行连接(join)。连接之后,返回值函数pthread_join()中中地第二个参数将是一个特殊值:PTHREAD_CANCELED。

示例程序

        程序清单32-1是一个使用pthread_cancel()的简单例子。主程序创建一个线程来执行无限循环,每次都在休眠一秒后打印循环计数器地值。(仅当向其发送取消请求或进程退出时,该线程才会终止。)同时,主程序将休眠3秒,随即向新创建地线程发送取消请求。程序运行结果如下:

程序清单32-1:调用pthread_cancel()取消线程 ---thread_cancel.c


#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

static void * threadFunc(void *arg)
{
    int j;
    printf("New thread started\n");  /*May be a cancellation point*/
    for(j=1;;j++){
        printf("loop %d\n",j);
        sleep(1);
    }

    /*NOTEREACHED*/
    return NULL;
}

int main(int argc,char *argv)
{
    pthread_t thr;
    int s;
    void *res;

    s=pthread_create(&thr,NULL,threadFunc,NULL);
    if(s!=0)
    {
        printf("pthread_create:");
        return -1;
    }
    sleep(3);//allow new thread to run a while

    s = pthread_cancel(thr);
    if(s!=0)
    {
        printf("pthread_cancel:");
        return -1;
    }
    s = pthread_join(thr,&res);
    if(s!=0)
    {
        printf("pthread_join:");
        return -1;
    }

    if(res == PTHREAD_CANCELED)
        printf("Thread was canceled\n");
    else
        printf("Thread was no canceled(should not happen!)\n");

    exit(EXIT_SUCCESS);

}

 32.4线程可取消性的检测

        在程序清单32-1中,由main()创建的线程会执行到属于取消点的函数(sleep(0s属于取消点,printf()可能也是),因而会接受取消请求。不过,假设线程执行的是一个不含取消点的循环(计算你记性循环)这时,线程永远也不会响应取消请求。

        函数pthread_testcancel()的目的很简单,就是产生一个取消点。线程如果是已有处于挂起状态的取消请求,那么只要调用该函数,线程就会随之终止。

#include <pthread.h>
void pthread_testcancel(void)

        当线程执行的代码为包含取消点时,可以周期性的调用pthread_testcancel()以确保对其他线程 向其发送的取消请求做出及时响应。

32.5清理函数(cleanup handler)

        一旦处于挂起状态的取消请求,线程在执行到取消点时,如果只是草草收场,这将会将共享变量以及Pthreads对象(例如互斥量)置于一种不一致状态,可能导致进程中其他线程产生错误、死锁,甚至造成程序崩溃。为规避这一问题,线程可以设置一个或多个清理函数,当线程遭取消时会自动运行这些函数,在线程zhongzhi-之前可执行诸如如何修改全局变量,解锁互斥量等动作。

        每个线程都可以拥有一个清理汉纳树栈,当线程遭取消时,会沿该栈自顶向下一次清理函数,首先会执行最近设置的函数,接着是次新的函数,以此类推。当执行完所有清理函数后,线程终止。

        函数pthread_cleanup_push()和pthread_cleanup_pop()分别负责向调用线程的清理函数栈添加和移除清理函数。

#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *),void *arg);
void pthread_cleanup_pop(int execute);

        pthread_cleanup_push()会见参数routine所含的函数地址天机道调用线程的清理函数栈顶。参数routine是一个函数指针,格式如下:

void routine(void *arg)
{
    /*Code to perform cleanup*/
}

        执行pthread_cleanup_push()时给定的arg值,会作为调用清理函数时的参数。其参数类型为void *,如果强制转换使用得当,那么通过该参数可以传入各种类型的数据。

        通常 ,线程如果在执行一段特殊代码时遭到取消,才需要执行清理动作。如果线程顺利执行完这段代码而未遭到取消,那么就不需要清理。所以,每个对pthread_cleanup_push()的调用都会伴随着对pthread_cleanup_pop()的调用。此函数从情理函数中移除最顶层的函数,如果参数execute非0,那么无论如何都会执行清理函数。在函数未遭取消而又希望执行清理动作时,这回很方便。

        尽管这里把pthread_cleanup_push()和 pthread_cleanup_pop()描述为函数,SUSv3却允许将他们实现为宏,可展开为分别由{和}所包裹的语句序列。并非所有的UNIX都这样做,不过包括Linux在内的很多系统都是使用宏来实现的。这意味着pthread_cleanup_push()和与其配对的pthread_cleanup_pop()属于同一个语法块,必须一一对应。(一旦以此方式来实现

),在对两者的调用见所声明的变量,起作用域将受限于这一语法块。)例如以下代码就不正确:

pthread_cleanup_push(func,arg);
......
if(cond){
    pthread_cleanup_pop(0);
}

         程序清单32-2提供了一个使用清理函数的简单例子。主程序创建线程,线程首先分配一块内存,并将其存储与buf中,接着锁定互斥量mtx。因为线程可能会遭到取消,所以调用pthread_cleanup_push()设置清理函数,并将存储与buf中的地址作为参数传入。如果执行到清理函数,那么清理函数就会释放内存并解锁互斥量

        线程接着进入循环,等待条件变量cond的通知,取决于可执行程序是否带有命令行参数,次循环会以以下两种方式结束。

  • 若无命令行参数,则由main()函数取消线程。此时,取消操作发生在对pthread_cond_wait()的调用中,此函数可见于程序清单32-1中,属于取消点。作为去掉动作的一部分,会自动调用pthread_cleanup_push()设置的清理函数。
  • 如果指定了命令函参数,那么将全局变量glob设置为非零后,通知条件变量,此时,线程会一直执行到pthread_cleanup_pop(),因为像此函数传入了分0参数,所以依然会调用清理函数。

程序清单32-2 使用清理函数  thread_cleanup.c

#include <stdio.h>
 #include <pthread.h>
 #include <stdlib.h>
 #include <unistd.h>
 static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
 static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
 static int glob;  //Predicate variable

 static void cleanupHandler(void *arg)  //gree memory pointed to by arg and unlock mutex
 {
    int s;
    printf("cleanup:freeing block at %p\n",arg);
    free(arg);

    printf("cleanup:unlocking mutex\n");

    s = pthread_mutex_unlock(&mtx);
    if(s!=0)
    {
        printf("pthread_muyex_unlock:");
        return ;
    }
 }

 static void  *threadFunc(void *arg)
 {
    int s;
    void *buf;  //nuffer allocated by thread

    buf = malloc(0x10000); //not a canceltion point
    printf("thread:allocated memory at %p\n",buf);;

    s = pthread_mutex_lock(&mtx);
    if(s!=0)
    {
        printf("pthread_muyex_lock:");
        return NULL;
    }
    pthread_cleanup_push(cleanupHandler,buf);

    while(glob == 0)
    {
        s = pthread_cond_wait(&cond,&mtx);
        if(s!=0)
        {
            printf("pthread_cond_wait:");
            return NULL;
        }
    }
    printf("thread:condition wait loop completed\n");
    pthread_cleanup_pop(1);  //executes cleanup handler
    return NULL;
 }

 int main(int argc,char *argv[])
 {
    pthread_t thr;
    void *res;
    int s;
    s=pthread_create(&thr,NULL,threadFunc,NULL);
    if(s!=0)
    {
        printf("pthread_create:");
        return -1;
    }

    sleep(2);  //give thread a chance to get start

    if(argc ==1){
        printf("main: about to cancel thread\n");
        s = pthread_cancel(thr);
        if(s!=0)
        {
            printf("pthread_cancel:");
            return -1;
        }
    }else{ /*signal condition variable*/
        printf("main: about to signal condition  variable\n");
        glob = 1;
        s = pthread_cond_signal(&cond);
        if(s!=0)
        {
            printf("pthread_cond_signal:");
            return -1;
        }
    }

    s = pthread_join(thr,&res);
    if(s!=0)
    {
        printf("pthread_join:");
        return -1;
    }

    if(res ==PTHREAD_CANCELED)
        printf("main: thread was canceled\n");
    else
         printf("main: thread terminated mormally\n");

    exit(EXIT_SUCCESS);
 }

主程序与遭终止线程建立连接,并报告线程是遭到取消还是正常终止。

如果执行程序清单32-2中程序且不带任何命令行参数,那么main()函数会调用pthread_cancel(0,清理函数也会得以自动执行。输出如下:

        如果运行该程序且带有命令函参数,那么main()将glob设置为1并通知条件变量,清理函数通过pthread_cleanup_pop()的调用执行,可以看到如下结果:

32.6 异步取消

        如果设定线程为可异步取消时,(取消性类型为PTHREAD_CANCEL_ASYNCHRONOUS),可意在任何点将其取消(亦即,执行任何机器指令时),取消动作不会延时到写一个u取消点才执行。

        异步取消的问题在于,尽管清理函数会依然得以执行,但小狐狸函数却无从得知现成的具体状态。程序32-2采用了延时取消类型,只有在执行到pthread_cond_wait()这一唯一的取消点时,线程才会取消。此时可知,已将buf初始化为指向新分配的内存块,并且锁定了互斥量mtx.不过,要是采用异步取消,就可以在任一点取消线程(例如调用malloc()之前,调用malloc()与锁定互斥量之间,或者锁定互斥量之后)。清理函数无法知道酱紫啊哪里发生取消动作,或者准确的说,清理函数不清楚需要执行哪些清理步骤。此外,线程也很可能在对malloc()的调用期间被取消,这极有可能造成后续的混乱

        作为一般性原则,可异步取消的线程不应该分配任何资源,也不能获取互斥量和锁。这导致大量库函数无法使用,其中就包括Pthreads函数的大部分。(SUSv3中有3处例外pthread-cancel()、pthread_setcancelstate()以及pthread_setcanceltype(),规范要求将他们实现为异步取消安全,亦即,实现必须确保在可异步取消的线程中可以安全调用他们。)换言之,异步取消功能鲜有应用成精,其中之一就是:取消在执行计算密集型循环的线程。

32.7总结

        函数pthread_cancel()允许某线程向另一线程发送取消请求,要求目标线程终止。

目标线程如何响应,取决于其取消性状态和类型。如果禁用线程的取消性状态,那么请求会保持挂起状态,直至将线程的取消性状态置为启用。如果启用取消性状态,那么线程何时响应请求则依赖于取消性类型。若类型为延迟取消,则在线程下一次调用某个取消点(SUSv3标准所规定的一系列函数之一)时,取消发生。如果为异步取消类型,取消动作随时可能发生(鲜有使用)。

        线程可以设置一个清理函数栈,其中的清理函数属于由卡法人员定义的函数,当线程遭到取消时,会自动调用这些函数以执行清理工作(例如恢复共享变量状态,或解锁互斥量)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值