如果进程中的任意线程调用了 exit
、_Exit
或者_exit
,那么整个进程就会终止。与此相类似,如果默认的动作是终止进程,那么,发送到线程的信号就会终止整个进程(12.8节将讨论信号与线程间是如何交互的)。
单个线程可以通过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-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);
}
图11-3 获得线程退出状态
运行图11-3中的程序,得到的结果是:
$ ./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
的线程试图使用该结构时,这个栈有可能已经被撤销,这块内存也已另作他用。
实例
图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_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 %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_fn1, 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 join with thread 1");
sleep(1);
printf("parent starting second thread\n");
err = pthread_create(&tid2, NULL, thr_fn2, NULL);
if (err != 0)
err_exit(err, "can't create thread 2");
sleep(1);
printfoo("parent:\n", fp);
exit(0);
}
图11-4 pthread_exit参数的不正确使用
在Linux上运行此程序,得到:
$ ./a.out
thread 1:
structure at 0x7f2c83682ed0
foo.a = 1
foo.b = 2
foo.c = 3
foo.d = 4
parent starting second thread
thread 2: ID is 139829159933696
parent:
structure at 0x7f2c83682ed0
foo.a = -2090321472
foo.b = 32556
foo.c = 1
foo.d = 0
当然,运行结果根据内存体系结构、编译器以及线程库的实现会有所不同。在Solaris上的结果类似:
$ ./a.out
thread 1:
structure at 0xffffffff7f0fbf30
foo.a = 1
foo.b = 2
foo.c = 3
foo.d = 4
parent starting second thread
thread 2: ID is 3
parent:
structure at 0xffffffff7f0fbf30
foo.a = -1
foo.b = 2136969048
foo.c = -1
foo.d = 2138049024
可以看到,当主线程访问这个结构时,结构的内容(在线程tid1的栈上分配的)已经改变了。注意第二个线程(tid2)的栈是如何覆盖第一个线程的栈的。为了解决这个问题,可以使用全局结构,或者用malloc
函数分配结构。
在Mac OS X上运行的结果有所不同:
$ ./a.out
thread 1:
structure at 0x1000b6f00
foo.a = 1
foo.b = 2
foo.c = 3
foo.d = 4
parent starting second thread
thread 2: ID is 4295716864
parent:
structure at 0x1000b6f00
Segmentation fault (core dumped)
在这种情况下,父进程试图访问已退出的第一个线程传给它的结构时,内存不再有效,这时得到的是SIGSEGV
信号。
FreeBSD上,父进程访问内存时,内存并没有被覆写,得到的结果是:
thread 1:
structure at 0xbf9fef88
foo.a = 1
foo.b = 2
foo.c = 3
foo.d = 4
parent starting second thread
thread 2: ID is 673279680
parent:
structure at 0xbf9fef88
foo.a = 1
foo.b = 2
foo.c = 3
foo.d = 4
虽然线程退出后,内存依然是完整的,但我们不能期望情况总是这样的。从其他平台上的结果中可以看出,情况并不都是这样的。
线程可以通过调用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节)安排退出是类似的。这样的函数称为线程清理处理程序(thread cleanup handler)。一个线程可以建立多个清理处理程序。处理程序记录在栈中,也就是说,它们的执行顺序与它们注册时相反。
#include <pthread.h>
void pthread_cleanup_push(void (*rtn)(void *), void*arg);
void pthread_cleanup_pop(int execute);
当线程执行以下动作时,清理函数rtn
是由pthread_cleanup_push
函数调度的,调用时只有一个参数arg
:
- 调用
pthread_exit
时; - 响应取消请求时;
- 用非零
execute
参数调用pthread_cleanup_pop
时。
如果 execute
参数设置为 0,清理函数将不被调用。不管发生上述哪种情况,pthread_cleanup_pop
都将删除上次pthread_cleanup_push
调用建立的清理处理程序。
这些函数有一个限制,由于它们可以实现为宏,所以必须在与线程相同的作用域中以匹配对的形式使用。pthread_cleanup_push
的宏定义可以包含字符{
,这种情况下,在 pthread_cleanup_pop
的定义中要有对应的匹配字符}
。
实例
图11-5给出了一个如何使用线程清理处理程序的例子。虽然例子是人为编造的,但它描述了其中涉及的清理机制。注意,虽然我们从来没想过要传一个参数0给线程启动例程,但还是需要把pthread_cleanup_pop
调用和pthread_cleanup_push
调用匹配起来,否则,程序编译就可能通不过。
#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 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)
err_exit(err, "can't create thread 1");
err = pthread_create(&tid2, NULL, thr_fn2, (void *)1);
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);
}
图11-5 线程清理处理程序
在Linux或者Solaris上运行图11-5中的程序会得到:
$ ./a.out
thread 1 start
thread 1 push complete
thread 2 start
thread 2 push complete
cleanup: thread 2 second handler
cleanup: thread 2 first handler
thread 1 exit code 1
thread 2 exit code 2
从输出结果可以看出,两个线程都正确地启动和退出了,但是只有第二个线程的清理处理程序被调用了。因此,如果线程是通过从它的启动例程中返回而终止的话,它的清理处理程序就不会被调用。还要注意,清理处理程序是按照与它们安装时相反的顺序被调用的。
如果在FreeBSD或者Mac OS X上运行相同的程序,可以看到程序会出现段异常并产生core文件。这是因为在这两个平台上,pthread_cleanup_push
是用宏实现的,而宏把某些上下文存放在栈上。当线程1在调用pthread_cleanup_push
和调用pthread_cleanup_pop
之间返回时,栈已被改写,而这两个平台在调用清理处理程序时就用了这个被改写的上下文。在SingleUNIX Specification中,函数如果在调用pthread_cleanup_push
和pthread_cleanup_pop
之间返回,会产生未定义行为。唯一的可移植方法是调用pthread_exit
。
现在,让我们了解一下线程函数和进程函数之间的相似之处。图11-6总结了这些相似的函数。
图11-6 进程和线程原语的比较
在默认情况下,线程的终止状态会保存直到对该线程调用pthread_join
。
如果线程已经被分离,线程的底层存储资源可以在线程终止时立即被收回。在线程被分离后,我们不能用pthread_join
函数等待它的终止状态,因为对分离状态的线程调用pthread_join
会产生未定义行为。可以调用pthread_detach
分离线程。
#include <pthread.h>
int pthread_detach(pthread_t tid);
//返回值:若成功,返回0;否则,返回错误编号
在下一章里,我们将学习通过修改传给pthread_create
函数的线程属性,创建一个已处于分离状态的线程。