11.5 线程终止


如果进程内任意线程调用了exit,_Exit或者是_exit,那么整个进程就会终止运行,类似地,当某信号的处理是终止进程的时候,该信号被发送到任一线程也将会终止整个进程(我们将在12.8节中更多地讨论信号与线程的交互)。
仅仅终止单个线程,而不是整个进程的方法有三种:

  1. 线程可以简单地从线程启动函数内返回,返回值就是线程的退出码;
  2. 线程可以被相同进程被的其他线程取消运行;
  3. 线程可以调用函数pthread_exit.
  1. #include <pthread.h>
  2. void pthread_exit(void *rval_ptr);

参数rval_ptr用于进程内其他调用函数pthread_join的线程。

  1. #include <pthread.h>
  2. int pthread_join(pthread_t thread, void **rval_ptr);
  3. 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展示了如何抓取一个已经退出线程的退出码的方法。

  1. #include "apue.h"
  2. #include <pthread.h>
  3. void *thr_fn1(void *arg)
  4. {
  5. printf("thread 1 returning\n");
  6. return ((void *)1);
  7. }
  8. void *thr_fn2(void *arg)
  9. {
  10. printf("thread 2 exiting\n");
  11. pthread_exit((void *)2);
  12. }
  13. int main(void)
  14. {
  15. int err;
  16. pthread_t tid1,tid2;
  17. void *tret;
  18. err = pthread_create(&tid1, NULL, thr_fn1, NULL);
  19. if(err != 0)
  20. err_exit(err, "can't create thread 1");
  21. err = pthread_create(&tid2, NULL, thr_fn2, NULL);
  22. if(err != 0)
  23. err_exit(err, "can't create thread 2");
  24. err = pthread_join(tid1, &tret);
  25. if(err != 0)
  26. err_exit(err, "can't join with thread 1");
  27. printf("thread 1 exit code %ld\n", (long)tret);
  28. err = pthread_join(tid2, &tret);
  29. if(err != 0)
  30. err_exit(err, "can't join with thread 2");
  31. printf("thread 2 exit code %ld\n", (long)tret);
  32. exit(0);
  33. }

Figure 11.3 获取线程退出状态
程序运行效果如下图所示:

  1. os@debian:~/UnixProgram/Chapter11$ ./11_3Exe
  2. thread 2 exiting
  3. thread 1 returning
  4. thread 1 exit code 1
  5. thread 2 exit code 2
  6. os@debian:~/UnixProgram/Chapter11$

Example

图11.4中的程序
展示了使用原子变量(通常分配在栈上)作为pthread_exit参数会出现的问题:

  1. #include "apue.h"
  2. #include <pthread.h>
  3. struct foo
  4. {
  5. int a,b,c,d;
  6. };
  7. void printfoo(const char *s, const struct foo *fp)
  8. {
  9. printf("%s", s);
  10. printf(" structure at 0x%lx\n", (unsigned long)fp);
  11. printf(" foo.a = %d\n", fp->a);
  12. printf(" foo.b = %d\n", fp->b);
  13. printf(" foo.c = %d\n", fp->c);
  14. printf(" foo.d = %d\n", fp->d);
  15. }
  16. void *thr_fun1(void *arg)
  17. {
  18. struct foo foo = {1,2,3,4};
  19. printfoo("thread 1:\n", &foo);
  20. pthread_exit((void *)&foo);
  21. }
  22. void *thr_fun2(void *arg)
  23. {
  24. printf("thread 2: ID is %lu\n", (unsigned long)pthread_self());
  25. pthread_exit((void*)0);
  26. }
  27. int main(void)
  28. {
  29. int err;
  30. pthread_t tid1,tid2;
  31. struct foo *fp;
  32. err = pthread_create(&tid1, NULL, thr_fun1, NULL);
  33. if(err != 0)
  34. err_exit(err, "can't create thread 1");
  35. err = pthread_join(tid1, (void *)&fp);
  36. if(err != 0)
  37. err_exit(err, "can;t create thread 2");
  38. sleep(1);
  39. printfoo("parent:\n", fp);
  40. printf("parent starting second thread\n");
  41. err = pthread_create(&tid2, NULL, thr_fun2, NULL);
  42. if(err != 0)
  43. err_exit(err, "can't create thread 2");
  44. sleep(1);
  45. printfoo("parent:\n", fp);
  46. exit(0);
  47. }

Figure 11.4 pthread_exit参数的错误使用示例
运行效果如下所示:

  1. os@debian:~/UnixProgram/Chapter11$ ./11_4Exe
  2. thread 1:
  3. structure at 0xb75a0380
  4. foo.a = 1
  5. foo.b = 2
  6. foo.c = 3
  7. foo.d = 4
  8. parent:
  9. structure at 0xb75a0380
  10. foo.a = -1218783672
  11. foo.b = -1217396748
  12. foo.c = -1218835600
  13. foo.d = -1218835600
  14. parent starting second thread
  15. thread 2: ID is 3076131696
  16. parent:
  17. structure at 0xb75a0380
  18. foo.a = -1217723378
  19. foo.b = -1218837612
  20. foo.c = -1217503244
  21. foo.d = -1217510812
  22. os@debian:~/UnixProgram/Chapter11$

可以看到,在线程1内分配的结构数据在线程退出的时候已经被破坏掉了,此外,线程2还会对第一个线程的堆栈进行改写。为了解决上述问题,有如下两种方法:

  • 使用全局数据结构;
  • 使用函数malloc进行空间分配;

一个线程可以请求同一进程内其他线程取消:

  1. #include <pthread.h>
  2. int pthread_cancel(pthread_t tid);
  3. Returns:0 if OK, error number on failure.

在默认情况下,函数pthread_cancel将会造成tid指定线程表现得像调用了函数pthread_exit((void *)PTHREAD_CANCELD)一样,但是线程也可以选择忽略或者是在被取消的时候执行其他控制流程,我们将在12.7节中进行细节讲述,注意函数pthread_cancel并不会等待线程退出,它只不过是发出一个请求。
线程可以安排一些函数在线程退出的时候被调用,类似于函数atexit在进程退出的时候可以安排一些函数被调用。对于线程而言,这些安排在线程退出是被调用的函数称为线程清理函数(thread cleanup handlers).可以同时注册多个线程清理函数到一个堆栈中,这意味着最后函数的执行顺序与他们被注册的顺序是相反的。

  1. #include <pthread.h>
  2. void pthread_cleanup_push(void (*rtn)(void *), void *arg);
  3. 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匹配,我们必须这样做,否则可能会存在编译不通过的情况。

  1. #include "apue.h"
  2. #include <pthread.h>
  3. void cleanup(void *arg)
  4. {
  5. printf("cleanup: %s\n", (char *)arg);
  6. }
  7. void *thr_fun1(void *arg)
  8. {
  9. printf("thread 1 start\n");
  10. pthread_cleanup_push(cleanup, "thread 1 first handler");
  11. pthread_cleanup_push(cleanup, "thread 1 second handler");
  12. printf("thread 1 push complete\n");
  13. if(arg)
  14. return((void *)1);
  15. pthread_cleanup_pop(0);
  16. pthread_cleanup_pop(0);
  17. return ((void*)1);
  18. }
  19. void *thr_fun2(void *arg)
  20. {
  21. printf("thread 2 start\n");
  22. pthread_cleanup_push(cleanup, "thread 2 first handler");
  23. pthread_cleanup_push(cleanup, "thread 2 second handler");
  24. printf("thread 2 push complete\n");
  25. if(arg)
  26. pthread_exit((void *)2);
  27. pthread_cleanup_pop(0);
  28. pthread_cleanup_pop(0);
  29. pthread_exit((void *)2);
  30. }
  31. int main(void)
  32. {
  33. int err;
  34. pthread_t tid1,tid2;
  35. long tret;
  36. err = pthread_create(&tid1, NULL, thr_fun1, (void *)1);
  37. if(err != 0)
  38. err_exit(err, "can't create thread 1");
  39. err = pthread_create(&tid2, NULL, thr_fun2, (void *)1);
  40. if(err != 0)
  41. err_exit(err, "can't create thread 2");
  42. err = pthread_join(tid1, (void *)&tret);
  43. if(err != 0)
  44. err_exit(err, "can't join with thread 1");
  45. printf("thread 1 exit code: %ld\n", tret);
  46. err = pthread_join(tid2, (void *)&tret);
  47. if(err != 0)
  48. err_exit(err, "can't join with thread 2");
  49. printf("thread 2 exit code : %ld\n", tret);
  50. exit(0);
  51. }

图11.5 线程清理函数
运行效果如下所示:

  1. os@debian:~/UnixProgram/Chapter11$ ./11_5Exe
  2. thread 2 start
  3. thread 1 start
  4. thread 1 push complete
  5. thread 2 push complete
  6. thread 1 exit code: 1
  7. cleanup: thread 2 second handler
  8. cleanup: thread 2 first handler
  9. thread 2 exit code : 2
  10. 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来分离一个线程。

  1. #include <pthread.h>
  2. int pthread_detach(pthread_t tid);
  3. Returns:0 if OK, error number on failure

正如我们在下一章中将会看到的那样,我们可以创建一个处于分离状态的线程,方法是修改传递给函数pthread_create的线程属性参数。





posted @ 2016-06-05 19:38  U201013687  阅读(155)  评论(0编辑  收藏  举报