线程之线程终止

如果进程中的任一线程调用了exit、_Exit或者_exit,那么整个进程就会终止。与此类似,如果信号的默认动作是终止进程,那么,把该信号发送到线程会终止整个进程。

单个线程可以通过下列三种方式退出,在不终止整个进程的情况下停止它的控制流。

(1)线程只是从启动例程中返回,返回值是线程的退出码。

(2)线程可以被同一进程中的其他线程取消。

(3)线程调用pthread_exit。

#include <pthread.h>
void pthread_exit(void *rval_ptr);

rval_ptr是一个无类型指针,与传给启动例程的单个参数类似。进程中的其他线程可以通过调用pthread_join函数访问到这个指针。

#include <pthread.h>
int pthread_join(pthread_t thread, void **rval_ptr);
返回值:若成功则返回0,否则返回错误编号

调用线程将一直阻塞,直到指定的线程调用pthread_exit、从启动例程中返回或者被取消。如果线程只是从它的启动例程返回,rval_ptr将包含返回码。如果线程被取消,由rval_ptr指定的内存单元就置为PTHREAD_CANCELED。

可以通过调用pthread_join自动把线程置于分离状态,这样资源就可以恢复。如果线程已经处于分离状态,pthread_join调用就会失败,返回EINVAL。

如果对线程的返回值并不感兴趣,可以把rval_ptr置为NULL。在这种情况下,调用pthread_join函数将等待指定的线程终止,但并不获取线程的终止状态。

实例

程序清单11-2说明了如何获取已终止的线程的退出码。

程序清单11-2 获得线程退出状态

#include "apue.h"
#include <pthread.h>

void *
thr_fn1(void *arg)
{
    printf("thread 1 returning\n");
    return((void *)1);
}

void *
thr_fn2(void *arg)
{
    printf("thread 2 exiting\n");
    pthread_exit((void *)2);
}

int 
main(void)
{
    int         err;
    pthread_t    tid1, tid2;
    void        *tret;

    err = pthread_create(&tid1, NULL, thr_fn1, NULL);
    if(err != 0)
        err_quit("can't create thread 1: %s\n", strerror(err));
    err = pthread_create(&tid2, NULL, thr_fn2, NULL);
    if(err != 0)
        err_quit("can't create thread 2: %s\n", strerror(err));
    err = pthread_join(tid1, &tret);
    if(err != 0)
        err_quit("can't join with thread 1: %s\n", strerror(err));
    printf("thread 1 exit code %d\n", (int)tret);
    err = pthread_join(tid2, &tret);
    if(err != 0)
        err_quit("can't join with thread 2: %s\n", strerror(err));
    printf("thread 2 exit code %d\n", (int)tret);
    exit(0);
}

运行程序清单11-2中的程序,得到的结果是:

未命名

可以看出,当一个线程通过调用pthread_exit退出或者简单地从启动例程中返回时,进程中的其他线程可以通过调用pthread_join函数获得该线程的退出状态

pthread_create和pthread_exit函数的无类型指针参数能传递的数值可以不止一个,该指针可以传递包含更复杂信息的结构的地址,但是注意这个结构所使用的内存在调用者完成调用以后必须仍然是有效的,否则就会出现无效或非法内存访问。

实例

程序清单11-3中的程序给出了用自动变量(分配在栈上)作为pthread_exit的参数时出现的问题。

程序清单11-3 pthread_exit参数的不正确使用

#include "apue.h"
#include <pthread.h>

struct foo {
    int a, b, c, d;
};

void 
printfoo(const char *s, const struct foo *fp)
{
    printf(s);
    printf("   structure at 0x%x\n", (unsigned)fp);
    printf("   foo.a = %d\n", fp->a);
    printf("   foo.b = %d\n", fp->b);
    printf("   foo.c = %d\n", fp->c);
    printf("   foo.d = %d\n", fp->d);
}

void *
thr_fn1(void *arg)
{
    struct foo foo = {1, 2, 3, 4};

    printfoo("thread 1:\n", &foo);
    pthread_exit((void *)&foo);
    printfoo("thread 1:\n", &foo);
}

void *
thr_fn2(void *arg)
{
    printf("thread 2: ID is %d\n", pthread_self());
    pthread_exit((void *)0);
}

int
main(void)
{
    int         err;
    pthread_t    tid1, tid2;
    struct foo    *fp;
    
    err = pthread_create(&tid1, NULL, thr_fn1, NULL);
    if(err != 0)
        err_quit("can't create thread 1: %s\n", strerror(err));
    err = pthread_join(tid1, (void *)&fp);
    if(err != 0)
        err_quit("can't join with thread 1: %s\n", strerror(err));
    sleep(1);
//    printf("parent starting  second thread\n");
        
//    err = pthread_create(&tid2, NULL, thr_fn2, NULL);
//    if(err != 0)
//        err_quit("cant' create thread 2: %s\n", strerror(err));
    sleep(1);
    printfoo("parent: \n", fp);
    exit(0);
}

运行程序清单11-3中的程序得到:

情况一:把带注释的行去掉注释也编译进程序中时的运行结果:

未命名

情况二:带注释的行不包括在程序中时的运行结果:

未命名

可以看出,当主线程访问这个结构时,结构的内容(在线tid1的栈上分配)已经改变。为了解决这个问题,可以使用全局结构,或者用malloc函数分配结构。

例如,若把struct foo foo = {1, 2, 3, 4}; 移到函数外,使其成为全局结构,则可得到如下结果:

未命名 

线程可以通过调用pthread_cancel函数来请求取消同一进程中的其他线程。

#include <pthread.h>
int pthread_cancel(pthread_t tid);
返回值:若成功则返回0,否则返回错误编号

在默认情况下,pthread_cancel函数会使得由tid标识的线程的行为表现为如同调用了参数为PTHREAD_CANCELED的pthread_exit函数,但是,线程可以选择忽略取消方式或是控制取消方式。注意,pthread_cancel并不等待线程终止,它仅仅提出请求。

线程可以安排它退出时需要调用的函数,这与进程可以用atexit函数安排进程退出时需要调用的函数是类似的。这样的函数称为线程清理处理程序(thread cleanup handler)。线程可以建立多个清理处理程序。处理程序记录在栈中,也就是说它们的执行顺序与它们注册时的顺序相反。

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

当线程执行以下动作时调用清理函数(调用参数为arg,清理函数rtn的调用顺序是由pthread_cleanup_push函数来安排的):

  • 调用pthread_exit时。
  • 相应取消请求时。
  • 用非零execute参数调用pthread_cleanup_pop时。

如果execute参数置为0,清理函数将不被调用。无论哪种情况,pthread_cleanup_pop都将删除上次pthread_cleanup_push调用建立的清理处理程序。

这些函数有一个限制,由于它们可以实现为宏,所以必须在与线程相同的作用域中以匹配对的形式使用,pthread_cleanup_push的宏定义可以包含字符{,在这种情况下对应的匹配字符}就要在pthread_cleanup_pop定义中出现。

实例

程序清单11-4显示了如何使用线程清理处理程序。需要把pthread_cleanup_pop调用和pthread_cleanup_push调用匹配起来,否则,程序编译可能通不过。

程序清单11-4 线程清理处理程序

#include "apue.h"
#include <pthread.h>

void 
cleanup(void *arg)
{
    printf("cleanup: %s\n", (char *)arg);
}

void *
thr_fn1(void *arg)
{
    printf("thread 1 start\n");
    pthread_cleanup_push(cleanup, "thread 1 first hanlder");    
    pthread_cleanup_push(cleanup, "thread 1 second handler");
    printf("thread 1 push complete\n");
    if(arg)
        return((void *)1);
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
    return((void *)1);
}

void *
thr_fn2(void *arg)
{
    printf("thread 2 start\n");
    pthread_cleanup_push(cleanup, "thread 2 first handler");
    pthread_cleanup_push(cleanup, "thread 2 second handler");
    printf("thread 2 push complete\n");
    if (arg)
        pthread_exit((void *)2);
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
    pthread_exit((void *)2);
}

int
main(void)
{
    int        err;    
    pthread_t    tid1, tid2;
    void        *tret;

    err = pthread_create(&tid1, NULL, thr_fn1, (void *)1);
    if(err != 0)
        err_quit("can't create thread 1: %s\n", strerror(err));
    err = pthread_create(&tid2, NULL, thr_fn2, (void *)1);
    if(err != 0)
        err_quit("can't create thread 2: %s\n", strerror(err));
    err = pthread_join(tid1, &tret);
    if(err != 0)
        err_quit("can't join with thread 1: %s\n", strerror(err));
    printf("thread 1 exit code %d\n", (int)tret);
    err = pthread_join(tid2, &tret);
    if(err != 0)
        err_quit("can't join with thread 2: %s\n", strerror(err));
    printf("thread 2 exit code %d\n", (int)tret);
    exit(0);
}

运行程序清单11-4中的程序会得到:

未命名

从输出结果可以看出,两个线程都正确地启动和退出了,但是只调用了第二个线程的清理处理程序,所以如果线程是通过从它的启动例程中返回而终止的话,那么它的清理处理程序就不会被调用,还要注意清理处理程序是按照与它们安装时相反的顺序被调用的。

现在可以开始看出线程函数和进程函数之间的相似之处。表11-1总结了这些相似的函数。

                                                  表11-1进程原语和线程原语的比较

2012080513265968

在默认情况下,线程的终止状态会保存一直等到对该线程调用pthread_join。如果线程已经处于分离状态(参考1http://linux.net527.cn/fuwuqiyingyong/Oracle/2012/0504/46837.html;参考2http://www.cnblogs.com/mydomain/archive/2011/08/14/2138454.htm,线程的底层存储资源可以在线程终止时立即被收回

在任何一个时间点上,线程是可结合的(joinable),或者是分离的(detached)。一个可结合的线程能够被其他线程收回其资源和杀死;在被其他线程回收之前,它的存储器资源(如栈)是不释放的。相反,一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放。

线程的分离状态决定一个线程以什么样的方式来终止自己。线程的默认属性,即为非分离状态(即可结合的,joinable,需要回收),这种情况下,原有的线程等待创建的线程结束;只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。程序员应该根据自己的需要,选择适当的分离状态。

当线程被分离时,并不能用pthread_join函数等待它的终止状态。对分离状态的线程进行pthread_join的调用会产生失败,返回EINVAL。pthread_detach调用可以用于使线程进入分离状态。

#include <pthread.h>
int pthread_detach(pthread_t tid);
返回值:若成功则返回0,否则返回错误编号

本篇博文内容摘自《UNIX环境高级编程》(第二版),仅作个人学习记录所用。关于本书可参考:http://www.apuebook.com/

posted @ 2014-01-14 15:49  ITtecman  阅读(1873)  评论(0编辑  收藏  举报