如果进程里的任何线程调用exit、_Exit或_exit,那么整个进程会终止。相似的,当默认的动作被设为终止进程时,一个发送给一个线程的信号将终止整个进程(我们在12.8节讨论更多关于信号和线程之间的交互)。
单个线程可以用三种方式退出,从而停止它的控制流,而不终止整个进程。
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允许我们等待指定的线程,但不得到线程的终止状态。
下面的代码展示了如何获取一个终止线程的退出码。
- #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) {
- printf("can't create thread 1: %s\n", strerror(err));
- exit(1);
- }
- err = pthread_create(&tid2, NULL, thr_fn2, NULL);
- if (err != 0) {
- printf("can't create thread 2: %s\n", strerror(err));
- exit(1);
- }
- err = pthread_join(tid1, &tret);
- if (err != 0) {
- printf("can't join with thread 1: %s\n", strerror(err));
- exit(1);
- }
- printf("thread 1 exit code %d\n", (int)tret);
- err = pthread_join(tid2, &tret);
- if (err != 0) {
- printf("can't join with thread 2: %s\n", strerror(err));
- exit(1);
- }
- printf("thread 2 exit code %d\n", (int)tret);
- exit(0);
- }
运行结果为:
$ ./a.out
thread 1 returning
thread 2 exiting
thread 1 exit code 1
thread 2 exit code 2
正如我们可以看到的,当一个线程通过调用pthread_exit退出或简单从启动例程返回时,退出状态可以被另一个线程通过调用pthread_join来获得。
传
递给pthread_create和pthread_exit的无类型的指针可以被用来传递比单个值更多。这个指针可以用来传递一个包含更复杂信号的结构
体的地址。小心当调用者完成时用作结构体的指针仍然有效。如果结构体在调用者的栈上分配,那么内存内容可能在结构体被使用时已经改变了。例如,如果一个线
程在它的栈上分配了一个结构体并把它的指针传递给pthread_exit,那么然后这个栈可能被销毁而它的内存被重用为其它东西,在
pthread_join尝试使用它时。
下面的代码展示了使用一个自动变量(在栈上分配)作为pthread_exit的参数的问题。
- #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);
- }
- 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) {
- printf("can't create thread 1:%s\n", strerror(err));
- exit(1);
- }
- err = pthread_join(tid1, (void *)&fp);
- if (err != 0) {
- printf("can't join with thread 1:%s\n", strerror(err));
- exit(1);
- }
- sleep(1);
- printf("parent starting second thread\n");
- err = pthread_create(&tid2, NULL, thr_fn2, NULL);
- if (err != 0) {
- printf("can't create thread 2:%s\n", strerror(err));
- exit(1);
- }
- sleep(1);
- printfoo("parent:\n", fp);
- exit(0);
- }
运行结果为:
thread 1:
structure at 0xb77dd380
foo.a = 1
foo.b = 2
foo.c = 3
foo.d = 4
parent starting second thread
thread 2: ID is -1216488592
parent:
structure at 0xb77dd380
foo.a = 0
foo.b = 2423756
foo.c = 2768884
foo.d = 2768884
当然,根据内存架构、编译器和线程库的实现,结果会有所不同。
正如我们可以看到的,结构体的内容(在线程tid1的栈上分配的)在主线程可以访问这个结构体时已经改变了。注意第二个线程(tid2)的栈如何覆写掉第一个线程的栈。为了解决这个问题,我们可以使用一个全局结构体或用malloc分配这个结构体。
一个线程可以请求同一进程内的另一个线程取消,通过调用pthread_cancel函数。
- #include <pthread.h>
- int pthread_cancel(pthread_t tid);
- 成功返回0,失败返回错误号。
在
默认情况下,pthread_cancel将导致tid指定的线程表现得好像它已经调用参数为PTHREAD_CANCELED的
pthread_exit。然而,一个线程可以选择忽略或控制它如何被取消。我们将在12.7节深入讨论。注意pthread_cancel不等待线程终
止。它只是发出请求。
一个线程可以安排当它退出时要调用的函数,和atexit函数(7.3节)安排在进程退出时所调用的函数的方法相似。这个函数被称为线程清理处理机。一个线程可以建立多个清理处理机。处理机被保存在一个栈里,这意味着它们以被注册的相反的顺序执行。
- #include <pthread.h>
- void pthread_cleanup_push(void (*rtn)(void *), void *arg);
- void pthread_cleanup_pop(int execute);
pthread_cleanup_push函数安排清理函数,rtn,随着单个参数,arg,而被调用,当线程执行以下某个动作时:
1、调用pthread_exit;
2、响应一个取消请求;
3、使用非0execute参数调用pthread_cleanup_pop。
如果execute参数被设为0,那么清理函数不会被调用。在各种情况下,pthread_cleanup_pop都删除由最后的pthraed_cleanup_push调用建立的清理处理机。
这些函数的一个限制是,因为它们可以被实现为宏,所以它们必须在线程的一些范围内成对地使用。pthread_cleanup_push的宏定义可以包括一个“{”字符,这种情况下要匹配pthread_cleanup_pop定义里的“}”字符。
下面的代码展示如何使用线程清理处理机。尽管这个例子有点勉强,但它展示了所使用的机制。注意尽管我们从未想要传一个非0参数给线程启动例程,但我们仍需要匹配pthread_cleanup_pop和pthread_cleanup_push;否则,程序可能不能编译。
- #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 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_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) {
- printf("can't create thread 1: %s\n", strerror(err));
- exit(1);
- }
- err = pthread_create(&tid2, NULL, thr_fn2, (void *)1);
- if (err != 0) {
- printf("can't create thread 2: %s\n", strerror(err));
- exit(1);
- }
- err = pthread_join(tid1, &tret);
- if (err != 0) {
- printf("can't join with thread 1: %s\n", strerror(err));
- exit(1);
- }
- printf("thread 1 exit code %d\n", (int)tret);
- err = pthread_join(tid2, &tret);
- if (err != 0) {
- printf("can't join with thread 2: %s\n", strerror(err));
- exit(1);
- }
- printf("thread 2 exit code %d\n", (int)tret);
- exit(0);
- }
运行结果为:
$ ./a.out
thread 1 start
thread 1 push complete
thread 1 exit code 1
thread 2 start
thread 2 push complete
cleanup: thread 2 second handler
cleanup: thread 2 first handler
thread 2 exit code 2
从输出我们可以看到两个线程都恰当地启动并退出,但是只有第二个线程的清理处理机被调用。因而,如果线程从它的主例程中返回,那么它的清理处理机不会被调用。还有注意清理处理机以它们被安装里的相当顺序被调用。
到现在为止,你应该开始看到线程函数和进程函数之间的相似之处了。下表总结了这些相似的函数:
进程和线程原始例程的比较进程原始例程 | 线程原始例程 | 描述 |
---|
fork | pthread_create | 创建一个新的控制流 |
exit | pthread_exit | 从已有的控制流中退出 |
waitpid | pthread_join | 从控制流得到退出状态 |
atexit | pthread_cancel_push | 注册在控制流退出时调用的函数 |
getpid | pthread_self | 得到控制流的ID |
abort | pthread_cancel | 请求控制流的异常退出 |
默认时,一个线程的终止状态被保留,直到pthread_join为那个线程调用。一个线程的底下存储可以在终止时立即回收,如果线程被分离。当一个线程被
分离时,pthread_join函数不能用来等待它的终止状态。一个未分离线程而调用的pthread_join会失败,返回EINVAL。我们可以调
用pthread_detach来分离一个线程。
- #include <pthread.h>
- int pthread_detach(pthread_t tid);
- 成功返回0,失败返回错误号。
正如我们将在下章看到的,我们可以创建一个处在分离状态的线程,通过仅修改传入pthread_create的线程属性。