线程
线程的一些基础知识,包括线程的创建、线程终止
线程的概念
典型的UNIX进程可以看成只有一个控制线程;一个进程在某一时刻只能做一件事情。有了多个控制线程后,在程序设计时就可以把进程设计成在某一时刻做不止一件事,每个线程处理各自独立的任务。这种方法有很多好处
- 简化处理异步事件的代码
- 分解复杂的问题提高整个程序的吞吐量
- 使用多线程改善响应时间
每个线程都包含有表示执行环境所必须的信息。其中包括进程中标识线程的线程ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno变量以及线程私有数据。
线程标识
每个线程都有一个线程ID。但跟进程不一样,进程ID在整个系统中是唯一的,线程ID只有在它所属的进程上下文中才有意义。
线程ID通过使用pthread_t数据类型表示,必须要使用一个函数对两个线程ID进行比较。
1 | #include <pthread.h> |
线程通过调用pthread_self函数获得自身的线程ID
1 | #include <pthread.h> |
线程创建
新增的线程可以通过调用pthread_create函数创建。
1 | #include <pthread.h> |
- 当pthread_create成功返回时,新创建线程的线程ID会被设置成tidp指向的内存单元。attr用于定制各种不同的线程属性(第12章介绍),设置为NULL,表示创建一个具有默认属性的线程。
- 新创建的线程从start_rtn函数的地址开始执行,该函数只有一个无类型指针参数arg。如果有多个参数传递,需要把这些参数放到一个结构中,然后传递这个结构的地址进去。
- 线程创建时不能保证执行顺序
如下程序创建了一个线程,打印了进程ID、新线程的线程ID以及初始线程的线程ID。
1 | #include "apue.h" |
运行结果如图所示
可以看到打印出了相同的进程ID,但在mac系统上线程ID不在相同地址段范围
线程终止
如果进程中的任意线程调用了exit、_Exit或者_exit,那么整个进程就会终止。
单个线程可以通过3种方式退出,可以在不终止整个进程的情况下,停止它的控制流。
- 线程可以简单的从启动例程中返回,返回值是线程的退出码。
- 线程可以被同一进程中的其他线程取消。
- 线程调用pthread_exit。
1 | #include <pthread.h> |
rval_ptr参数是一个无类型指针,进程中的其他线程可以通过调用pthread_join函数访问到这个指针
1 | #include <pthread.h> |
- 调用线程将一直阻塞,直到指定的线程调用pthread_exit、从启动例程中返回或者被取消。如果线程简单地从它的启动例程返回,rval_ptr就包含返回码。如果线程被取消,由rval_ptr指定的内存单元就设置为PTHREAD_CANCELED。
- 通过调用pthread_join自动把线程置于分离状态,这样资源就可以恢复。如果线程已经处于分离状态,pthread_join就会调用失败,返回EINVAL
如下程序展示了如何获取已经终止线程的退出码
1 | #include "apue.h" |
运行结果如下:
线程可以通过调用pthread_cancel函数来请求取消同一进程中的其他线程
1 | #include <pthread.h> |
pthread_cancel并不等待线程终止,它仅仅提出请求
线程可以安排它退出时需要调用的函数(线程清理处理程序)。一个线程可以建立多个清理处理程序。处理程序记录在栈中,他们的执行顺序与他们注册时相反。
1 | #include <pthread.h> |
当线程执行以下动作时,清理函数rtn是由pthread_cleanup_push函数调度的,调用时只有一个参数arg:
- 调用pthread_exit时
- 响应取消请求时
- 用非零execute参数调用pthread_cleanup_pop时
如果execute参数设置为0,清理函数将不被调用。
如下程序是使用线程清理处理程序的例子
#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)
return ((void *)2);
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
return ((void *)2);
}
int main(int argc, const char *args[])
{
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);
}
运行结果如下
mac 下会产生core文件。这是因为在mac平台上,pthread_cleanup_push是用宏实现的,而宏把某些上下文存放到栈上。当线程1在调用pthread_cleanup_push和调用pthread_cleanup_pop之间返回时,栈已被改写,而这个平台在调用清理处理程序时就用了这个被改写的上下文。在Single UNIX Specification中,函数如果在调用pthread_cleanup_push和pthread_cleanup_pop之间返回,会产生未定义行为。唯一的可移植方法是调用pthread_exit.