概述
前面有一篇文章专门讲述了进程创建,监控和终止,这一篇文章进一步来谈谈线程的创建和同步等操作(这里指的是POSIX规范下的线程,即Pthreads)。和探讨进程的文章类似,还是通过讲述相关调用的使用和注意事项来推进,并提供一些实例来做说明。
第一部分:线程的概述,以及和进程的比较
================================================
首先需要明确一点,进程和线程都是内核调度的实体,只是他们各自之间的属性共享程度不同。这也就决定了他们之间必然存在多种通信机制,这会在后面的一篇文章中专门介绍。这里我只是想强调他们都是内核调度的实体,虽然二者有区别,但真的很类似。一个进程可以包含多个子进程,每一个进程又可以包含多个线程(不管是父进程还是子进程),进程和线程都是为了实现并行而生。意识到他们的共同点能帮助我们更好的理解他们的区别。同一进程中的所有线程均会独立执行相同的程序(注意,这里说的是相同的程序,因为它们共用同一份代码段。),且共享同一份全局内存区域(初始化数据段,未初始化数据段和堆)。和进程一样目的在于实现并行,但相较于进程,线程有两个突出的优点:
1. 线程间数据共享很方便。因为同一程序的所有线程共享同一份全局内存区域,所以只需要将需要共享的数据以全局变量或者动态分配的变量的方式存储即可实现在线程之间的共享(对于这一点的负面影响——竞争问题,会在后面提到)。
2. 创建线程要比创建进程快,因为线程相对于进程有更多属性是共享的,在创建线程的时候不需要重新设置这些属性。这里列举几个常用的属性:内存块上的程序代码段、全局内存和堆;进程ID、父进程ID、进程组ID、会话ID、控制终端;打开的文件描述符、信号处置和资源限制等。
第二部分:线程的基本调用(常用Pthread API)
================================================
在使用线程之前,有三点要说明一下:
1. 线程中使用的errno实际上是一个宏而不是一个全局变量。原因很简单,线程之间的全局变量是共享的,所以为了保证线程的errno的真实性需要实现独立。而线程中使用的errno宏定义即维持了原来的使用习惯也确保了线程间的独立性。
2. 所有Pthread API的返回值,0表示成功,失败返回一个正值,这个值等于errno宏的返回值。
3. 源文件包含头文件 pthread.h ,编译时加上 -pthread选项。
下面是常用的 Pthread API 及其简单说明,重要的是后面备注里的内容,这是使用这些接口时该注意的地方:
创建线程:
int pthread_create(pthread_t *restrict thread,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void*), void *restrict arg);
终止线程:
void pthread_exit(void *value_ptr);
int pthread_cancel(pthread_t thread);
备注:exit 和 return 当然也可以导致线程退出。此外,pthread_exit 的参数 value_ptr 不应分配在线程栈中。(因为线程退出后线程栈会被销毁,导致这一返回值不能被正常使用。)
获取/比较线程ID:
pthread_t pthread_self(void);
int pthread_equal(pthread_t t1, pthread_t t2);
备注:为什么会有 pthread_equal 这个函数呢?这是因为Pthread线程的数据类型的实现是不透明的,SUSv3并未规定如何实现Pthread线程的数据类型实现。我们不能直接用"=="来比较两个线程好,标准的可移植用法是使用 pthread_equal 接口。
线程清理:
int pthread_detach(pthread_t thread);
int pthread_join(pthread_t thread, void **value_ptr);
备注:和进程一样,线程结束后如果不清理,也会变成僵尸。上面的两个接口可以实现清理线程,区别在于 pthread_detach 是使线程处于分离状态,这样在线程结束时就可以自动清理;而 pthread_join 则类似于 waitpid 调用,可用于等待指定线程结束并清理之。关于他们的使用注意以下几点:
1. 分离的线程不能再被 pthread_join 连接,也无法恢复到可连接状态。
2. 分离只是设置指定线程不能被 pthread_join 连接以及可以自动清理的特性,主线程的 return 和其他线程的 exit 动作仍然可以促使已分离线程退出。
3. 不同于 waitpid 只能被父进程用来监控子进程状态,线程之间可以相互使用 pthread_join 获取彼此状态。
4. pthread_join 调用只能以阻塞的方式工作。
下面举一个实例,来说明线程的使用的基本方式。实例中,线程1负责打印标题栏,线程2负责运算(0+1+2+3+4+……+n),线程1启动后将自身设置为分离状态,而线程2运算结束后调用函数 pthread_exit 退出。主线程等待线程2结束后获取其计算结果,并打印相关数据。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
static int glob = 0;
static void * fun1(void * arg)
{
pthread_detach(pthread_self());
printf("%8s%8s%8s\n","start","end","sum");
return 0;
}
static void * fun2(void * arg)
{
int loops =*(int *)arg;
int j;
for(j = 1; j <= loops; j++)
glob = glob + j;
pthread_exit(&glob);
}
int main(int argc, char* argv[])
{
pthread_t t1,t2;
int loops,*sum_p;
if (argc < 2) {
printf("Please excute with a int argument.\n");
exit(1);
}
loops = atoi(argv[1]);
if (pthread_create(&t1,NULL,fun1,NULL) != 0) {
printf("Creat thread 1 failed.\n");
exit(1);
}
if (pthread_create(&t2,NULL,fun2,&loops) != 0) {
printf("Creat thread 2 failed.\n");
exit(1);
}
if (pthread_join(t2,(void**)&sum_p) != 0) {
printf("Join thread 2 failed.\n");
exit(1);
}
printf("%8d%8d%8d\n",0,loops,*sum_p);
exit(0);
}
运行结果如下:
[root@Brandy thread]# gcc -pthread -o thread_normal thread_normal.c [root@Brandy thread]# ./thread_normal 10 start end sum 0 10 55
第三部分:线程之间的同步问题
================================================
正是由于线程之间共享了全局内存区,那么必然出现多线程之间对共享变量的竞争。Pthread中的互斥量和条件变量正是用于解决这个问题的良药,下面针对互斥量和条件变量做以下几点说明:
1. 互斥量属于pthread_mutex_t 类型的变量,在使用之前必须对其初始化。初始化的方法有两种。静态初始化:互斥量为全局变量,定义时直接初始化为 PTHREAD_MUTEX_INITIALIZER;动态初始化:互斥量动态分配在堆或栈中,或者是不使用默认属性的全局变量。
静态初始化:pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER
动态初始化:int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
2. 互斥量使用注意事项:线程不应对已经被自己加锁的互斥量再次加锁;线程不应对其他线程加锁的互斥量进行解锁;线程不应对尚未加锁的互斥量做解锁操作。
3. 互斥量使用的良好习惯:严格遵循先加锁后解锁的操作;解锁前要检查是否已经锁定;存在多个互斥量时最好设置互斥量之间的层级(也就是每个线程都按照同一个顺序锁定互斥量),以防出现死锁。
4. 对于动态初始化的互斥量使用完毕后,需要使用函数 pthread_mutex_destroy() 将其销毁,而经该函数销毁的互斥量可以再次使用函数 pthread_mutex_init() 初始化。
5. 互斥量的加锁解锁操作相关调用:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict abs_timeout);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
说明:pthread_mutex_lock 是最普通的上锁操作,相形之下 pthread_mutex_trylock 遇到已锁定的情况不会阻塞,而直接返回EBUSY的错误。pthread_mutex_timedlock 可以设置阻塞的时间,超出时间则返回ETIMEDOUT 错误。
6. 条件变量的初始化方法和互斥量类似也有两种:
静态初始化:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
动态初始化:int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
7. 条件变量必须结合互斥量一起使用,条件变量就某个共享变量的状态变化发出通知,而互斥量则确保对共享变量的访问的互斥性。
8. 对于动态初始化的条件变量,使用完毕后需要调用函数 pthread_cond_destroy() 进行销毁。已经销毁的条件变量,只要其所在的堆栈没有被释放,仍然可以再次调用函数 pthread_cond_init() 进行初始化。
9. 使用条件变量的相关调用:
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);
说明:pthread_cond_signal 只会唤醒至少一条线程,适用于所有线程执行完全相同的任务的情况下;pthread_cond_broadcast 会唤醒所有等待线程,适用于每个线程执行任务关联到共享变量的判断条件不同而执行不同任务的情况。pthread_cond_wait 和 pthread_cond_timedwait 就没什么好说的了。
上述内容足以应对简单的线程使用问题,如果对线程安全函数(函数的可重入性)以及线程特有数据的内容感兴趣,可以看看《Linux/UNIX系统编程手册》。
第四部分:线程的取消
================================================
谈到线程取消,其实并不那么简单。虽然我们知道 exit 和 return 都会导致线程退出,但是要控制某个线程取消仍有不少细节需要注意:
1. 函数 pthread_cancel() 允许某线程向另一个线程发送取消请求,要求目标线程取消。
2. 目标线程的响应,取决于该线程的取消性状态和类型,设置函数如下:
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
说明:新建线程的默认取消性状态和类型分别为 PTHREAD_CANCEL_ENABLE 和 PTHREAD_CANCEL_DEFERED,分别表示线程可以取消,类型为延迟型(取消请求被挂起,直至遇到下一个取消点,所谓的取消点实际上是一些特定的系统调用在线程中被调用的位置)。取消性状态还可以设置为 PTHREAD_CANCEL_DISABLE 表示线程不可取消,线程接收到的取消请求会被挂起直到线程取消性状态被设置为 PTHREAD_CANCEL_ENABLE 。取消性类型还可以被设置为 PTHREAD_CANCEL_ASYNCHRONOUS ,异步取消表示线程接收到取消请求后可能会在任何时点取消线程。
3. 可以给线程设置一个清理函数栈,其中的清理函数是由开发人员定义的函数,在线程取消时自动调用该栈内函数进行线程清理操作,例如回复共享变量状态和解锁互斥量等操作。相关调用如下:
void pthread_cleanup_push(void (*routine)(void*), void *arg);
void pthread_cleanup_pop(int execute);
等待上传实例。