APUE读书笔记-11线程(3)
5、线程终止
当进程的任何一个线程调用 exit
, _exit
或者 _Exit
的时候,整个进程都会被终止。类似地,当信号的默认处理动作是终止进程的时候,给一个线程发送信号会导致整个进程的终止。我们后面会讨论线程和信号的交互。
正常地终止一个线程而不终止整个进程,有三个方法:
-
线程从它的起始函数中正常地返回。这时候,线程的退出码就是返回值。
-
线程被同一个进程中的其他线程取消。
-
线程调用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_prt
将会包含相应的返回码;如果线程被取消, rval_ptr
指向的内存地址将会被设置为 PTHREAD_CANCELED
.
调用 pthread_join
会自动地把线程置于 detached
状态,以便恢复线程的资源(稍后会讲到)。如果线程已经是 detached
状态了,那么 pthread_join
会失败并且返回 EINVAL
.
如果我们对线程的返回值不感兴趣,那么我们可以把rval_ptr设置为空,这样会等待指定的线程但是不获取线程的退出状态。
举例
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);
}
运行如下:
$ ./a.out
thread 1 returning
thread 2 exiting
thread 1 exit code 1
thread 2 exit code 2
可以看出,一个线程如果从 start
函数中退出,或者调用 pthread_exit
退出,那么其他的进程可以通过 pthread_join
来获取进程的结束状态。
我们可以给 pthread_create
和 pthread_exit
传递一个无类型的指针,这样指针可以指向复杂的结构,包含更多得信息。需要注意的是当线程结束的时候,指针指向的位置应该还是合法的。如果指针指向的位置是在栈上面分配的,那么当线程结束之后,栈内容就不确定了。而调用 pthread_join
的调用者却使用了刚才栈所在地址的内容。
线程可以通过调用 pthread_cancel
函数请求同一个进程中的其他线程被取消。
#include <pthread.h>
int pthread_cancel(pthread_t tid);
返回值:0表示成功,错误码表示失败。
默认情况下 pthread_cancel
调用会导致 tid
指定的线程表现的像是自己调用具有 PTHREAD_CANCELED
参数的 pthread_exit
一样。线程也可以选择忽略其他线程对它的取消,以及选择如何被取消以后会讲到。然而 pthread_cancel
不会等待线程结束,它只是做一个请求。
线程可以设置退出时候调用的函数,这个和进程使用 atexit
函数设置进程退出时候调用得函数类似。这些函数叫做“线程清理函数”,可以为线程设置多个清理函数,这些清理函数被记录在栈中,这也意味这这些函数的调用次序和它们被注册的次序相反。
#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
- 响应取消请求
- 使用非0的
execute
参数调用pthread_cleanup_pop
.
当 pthread_cleanup_pop
参数为0的时候,不会调用清理函数,这个时候会把最后一次调用 pthread_cleanup_push
的函数去掉。
这些函数的使用限制就是它们是使用宏实现的,它们必须在一个线程的同一个作用域内成对匹配使用, pthread_cleanup_push
宏包含是一个 {
, pthread_cleanup_pop
宏包含一个 }
。
举例:
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_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);
}
上面的例子展示了如何使用线程的清理函数。需要注意的是尽管我们没有打算给线程的启动函数传递非0参数,我们还是需要调用 pthread_cleanup_pop
函数来匹配 pthread_cleanup_push
函数,否则程序无法编译通过。
运行这个程序的输出是:
$ ./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
从输出中我们可以看到,两个线程都正常地启动和退出了,但是只有第二个线程调用了清理函数。因此,如果线程是通过从启动函数中正常返回而终止的话,就不会执行清理函数。并且我们也应该注意启动函数的调用次序和它们被安装的次序是相反的。
实际线程和进程有许多类似的函数,下表给出了这个对比。
进程和线程相关函数的对比
+-------------------------------------------------------------------------------------------------------+
| Process primitive | Thread primitive | Description |
|-------------------+---------------------+-------------------------------------------------------------|
| fork | pthread_create | create a new flow of control |
|-------------------+---------------------+-------------------------------------------------------------|
| exit | pthread_exit | exit from an existing flow of control |
|-------------------+---------------------+-------------------------------------------------------------|
| waitpid | pthread_join | get exit status from flow of control |
|-------------------+---------------------+-------------------------------------------------------------|
| atexit | pthread_cancel_push | register function to be called at exit from flow of control |
|-------------------+---------------------+-------------------------------------------------------------|
| getpid | pthread_self | get ID for flow of control |
|-------------------+---------------------+-------------------------------------------------------------|
| abort | pthread_cancel | request abnormal termination of flow of control |
+-------------------------------------------------------------------------------------------------------+
默认来说,一个线程的终止状态会一直保留到 pthread_join
被调用。一个终止的线程所占的内存会在 detached
的时候立即被回收,当一个线程被 detached
的时候,不能使用 pthread_join
函数等待获取它的终止状态。对一个 detached
的线程调用 pthread_join
会失败,并且返回 EINVAL
。我们可以使用 pthread_detach
来将一个线程 detach
.
#include <pthread.h>
int pthread_detach(pthread_t tid);
返回:如果成功返回0,如果失败返回错误编号。
后面我们可以看到,我们可以通过修改传递给 pthread_create
的线程属性参数来建立一个开始就处于 detached
状态的线程。