11.5 线程终止
如果进程内任意线程调用了exit,_Exit或者是_exit,那么整个进程就会终止运行,类似地,当某信号的处理是终止进程的时候,该信号被发送到任一线程也将会终止整个进程(我们将在12.8节中更多地讨论信号与线程的交互)。
仅仅终止单个线程,而不是整个进程的方法有三种:
- 线程可以简单地从线程启动函数内返回,返回值就是线程的退出码;
- 线程可以被相同进程被的其他线程取消运行;
- 线程可以调用函数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);
Returns:0 if OK,error number on failure
调用线程将一直被阻塞,直到指定线程调用函数pthread_exit,从其启动函数内返回,或者是被取消。如果线程简单地从线程函数内返回。rval_ptr将会包含返回码。如果线程被取消,rval_ptr指定的位置将被设置为PTHREAD_CANCELD.
通过调用函数pthread_join,会自动将thread指定的线程放到detached state(稍后会进行讨论),以至于其资源是可以恢复的。如果线程已经处于detached state,那么函数pthtread_join就会失败,并且返回错误编码EINVAL,虽然这一行为是实现指定的。
如果我们对于线程的返回值不感兴趣,我们可以设置rval_ptr的值为NULL,在这种情况下,调用pthread_join将会允许我们等待特定线程,但是并不获取线程终止状态。
Example
图11.3展示了如何抓取一个已经退出线程的退出码的方法。
#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_exit(err, "can't create thread 1");
err = pthread_create(&tid2, NULL, thr_fn2, NULL);
if(err != 0)
err_exit(err, "can't create thread 2");
err = pthread_join(tid1, &tret);
if(err != 0)
err_exit(err, "can't join with thread 1");
printf("thread 1 exit code %ld\n", (long)tret);
err = pthread_join(tid2, &tret);
if(err != 0)
err_exit(err, "can't join with thread 2");
printf("thread 2 exit code %ld\n", (long)tret);
exit(0);
}
Figure 11.3 获取线程退出状态
程序运行效果如下图所示:
os@debian:~/UnixProgram/Chapter11$ ./11_3Exe
thread 2 exiting
thread 1 returning
thread 1 exit code 1
thread 2 exit code 2
os@debian:~/UnixProgram/Chapter11$
Example
图11.4中的程序
展示了使用原子变量(通常分配在栈上)作为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", s);
printf(" structure at 0x%lx\n", (unsigned long)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_fun1(void *arg)
{
struct foo foo = {1,2,3,4};
printfoo("thread 1:\n", &foo);
pthread_exit((void *)&foo);
}
void *thr_fun2(void *arg)
{
printf("thread 2: ID is %lu\n", (unsigned long)pthread_self());
pthread_exit((void*)0);
}
int main(void)
{
int err;
pthread_t tid1,tid2;
struct foo *fp;
err = pthread_create(&tid1, NULL, thr_fun1, NULL);
if(err != 0)
err_exit(err, "can't create thread 1");
err = pthread_join(tid1, (void *)&fp);
if(err != 0)
err_exit(err, "can;t create thread 2");
sleep(1);
printfoo("parent:\n", fp);
printf("parent starting second thread\n");
err = pthread_create(&tid2, NULL, thr_fun2, NULL);
if(err != 0)
err_exit(err, "can't create thread 2");
sleep(1);
printfoo("parent:\n", fp);
exit(0);
}
Figure 11.4 pthread_exit参数的错误使用示例
运行效果如下所示:
os@debian:~/UnixProgram/Chapter11$ ./11_4Exe
thread 1:
structure at 0xb75a0380
foo.a = 1
foo.b = 2
foo.c = 3
foo.d = 4
parent:
structure at 0xb75a0380
foo.a = -1218783672
foo.b = -1217396748
foo.c = -1218835600
foo.d = -1218835600
parent starting second thread
thread 2: ID is 3076131696
parent:
structure at 0xb75a0380
foo.a = -1217723378
foo.b = -1218837612
foo.c = -1217503244
foo.d = -1217510812
os@debian:~/UnixProgram/Chapter11$
可以看到,在线程1内分配的结构数据在线程退出的时候已经被破坏掉了,此外,线程2还会对第一个线程的堆栈进行改写。为了解决上述问题,有如下两种方法:
- 使用全局数据结构;
- 使用函数malloc进行空间分配;
一个线程可以请求同一进程内其他线程取消:
#include <pthread.h>
int pthread_cancel(pthread_t tid);
Returns:0 if OK, error number on failure.
在默认情况下,函数pthread_cancel将会造成tid指定线程表现得像调用了函数pthread_exit((void *)PTHREAD_CANCELD)一样,但是线程也可以选择忽略或者是在被取消的时候执行其他控制流程,我们将在12.7节中进行细节讲述,注意函数pthread_cancel并不会等待线程退出,它只不过是发出一个请求。
线程可以安排一些函数在线程退出的时候被调用,类似于函数atexit在进程退出的时候可以安排一些函数被调用。对于线程而言,这些安排在线程退出是被调用的函数称为线程清理函数(thread cleanup handlers).可以同时注册多个线程清理函数到一个堆栈中,这意味着最后函数的执行顺序与他们被注册的顺序是相反的。
#include <pthread.h>
void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute);
当线程执行如下动作中的任意一个时,函数pthread_cleanup_push的作用就是调度清理函数rtn,跟着一个参数arg被调用:
- 调用函数pthread_exit;
- 相应线程取消请求;
- 以一个非零参数调用函数pthread_cleanup_pop;
如果函数pthread_cleanup_pop参数是零,那么清理函数并不会被调用,在这种情况下,函数pthread_cleanup_pop将会清理最后一次函数pthread_cleanup_push函数建立的清理函数。
对于上述函数调用的一个限制是:他们必须以成对方式在线程范围内调用,因为它们可能是使用宏定义实现的,在宏定义pthread_cleanup_push中可能包含了一个字符{,同时在宏定义pthread_cleanup_pop中包含了字符}.
Example
图11.5展示了如何使用清理函数。虽然这个例子有些多余,但是其中涉及到了相关的机制。虽然说调用函数pthread_cleanup_push(NULL,NULL)并不是我们想要的,但是有时候需要与pthread_cleanup_pop匹配,我们必须这样做,否则可能会存在编译不通过的情况。
#include "apue.h"
#include <pthread.h>
void cleanup(void *arg)
{
printf("cleanup: %s\n", (char *)arg);
}
void *thr_fun1(void *arg)
{
printf("thread 1 start\n");
pthread_cleanup_push(cleanup, "thread 1 first handler");
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_fun2(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;
long tret;
err = pthread_create(&tid1, NULL, thr_fun1, (void *)1);
if(err != 0)
err_exit(err, "can't create thread 1");
err = pthread_create(&tid2, NULL, thr_fun2, (void *)1);
if(err != 0)
err_exit(err, "can't create thread 2");
err = pthread_join(tid1, (void *)&tret);
if(err != 0)
err_exit(err, "can't join with thread 1");
printf("thread 1 exit code: %ld\n", tret);
err = pthread_join(tid2, (void *)&tret);
if(err != 0)
err_exit(err, "can't join with thread 2");
printf("thread 2 exit code : %ld\n", tret);
exit(0);
}
图11.5 线程清理函数
运行效果如下所示:
os@debian:~/UnixProgram/Chapter11$ ./11_5Exe
thread 2 start
thread 1 start
thread 1 push complete
thread 2 push complete
thread 1 exit code: 1
cleanup: thread 2 second handler
cleanup: thread 2 first handler
thread 2 exit code : 2
os@debian:~/UnixProgram/Chapter11$
从输出可以看出,两个线程都正常运行并退出了,但是仅仅只有第二个线程的清理函数被调用执行了,也就是说,如果线程通过return的形式返回的话,其清理函数并不会被调用,虽然这一行为与实现有关。同时注意到,清理函数的调用顺序确实是它们安装顺序的反序。
如果我们在FreeBSD或者是Mac OS X上运行上述程序,我们将会看到程序会出现一个segmentation violation and drops core错误.这是因为在这两个系统上,pthread_cleanup_push函数是使用宏定义存储一些上下文都堆栈中实现的,当线程1在pthread_cleanup_push与pthread_cleanup_pop之间返回的时候,堆栈被修改了,这些平台却尝试使用这些被破坏的堆栈上下文。在the Single Unix Specification中,从一个配对的pthread_cleanup_push与pthread_cleanup_pop中间返回会导致未定义的结果。因此可移植的实现方法是在这两个函数之间进行返回需要调用函数pthread_exit.
现在,我们应该总结一下线程函数与进程函数之间的相似性了,图11.6总结了这些相似函数:
进程函数 | 线程函数 | 描述 |
---|---|---|
fork | pthread_create | 创建控制流 |
exit | pthread_exit | 退出控制流 |
waitpid | pthread_join | 获取控制流退出状态 |
atexit | pthread_cleanup_push | 注册在控制流退出时调用的函数 |
getpid | pthread_self | 获取控制流ID |
abort | pthread_cancel | 请求异常终止控制流 |
图11.6 进程函数与线程函数对比
默认情况下,线程的终止状态会一直保留直到我们对其调用函数pthread_join。线程的潜在的存储可以在终止的时候立即回收,前提是线程已经被分离(detached).在一个线程被分离以后,我们不能使用函数pthread_join来等待其终止状态,因为对于一个已经分离的线程调用函数pthread_join的行为是未定义的,我们可以使用函数pthread_detach来分离一个线程。
#include <pthread.h>
int pthread_detach(pthread_t tid);
Returns:0 if OK, error number on failure
正如我们在下一章中将会看到的那样,我们可以创建一个处于分离状态的线程,方法是修改传递给函数pthread_create的线程属性参数。