B.1 概述
本附录汇总基本的Posix线程函数。在传统的Unix模型中,当一个进程需要由另一个实体来执行某件事时,它就fork一个子进程,让子进程去进行处理。举例来说,Unix下的大多数网络服务器程序就是这么编写的。
尽管这种模式已成功地使用了很多年,但是fork仍然暴露出了以下问题。
fork的开销很大。内存映像要从父进程复制到子进程,所有描述符要在子进程中复制一份,等等。当前的系统实现使用一种称为写时复制(copy-on-write)的技术,可避免父进程数据空间一开始就向子进程复制,直到子进程确实需要自己的副本为止。尽管有这种优化技术,fork的开销仍然很大。
fork子进程后,需要用进程间通信(IPC)在父子进程之间传递信息。fork之前由父进程准备好的信息容易传递,因为子进程是从父进程的数据空间及所有描述符的一个副本开始运行的。但是从子进程返回信息给父进程却颇费周折。
线程有助于解决这两个问题。线程有时称为轻权进程(lightweight process),因为线程比进程“权轻”。 [1] 也就是说,创建线程可能比创建进程快10~100倍。
一个进程内的所有线程共享同一个全局内存空间。这使得线程间很容易共享信息,但是这种容易性也带来了同步(synchronization)问题。一个进程内的所有线程不光共享全局变量,以下信息也是它们所共享的:
进程指令;
大多数数据;
打开的文件(例如描述符);
信号处理程序和信号处置;
当前工作目录;
用户ID和组ID。
但是下列信息却是特定于每个线程的:
线程ID;
寄存器集合,包括程序计数器和栈指针;
栈(用于存放局部变量和返回地址);
errno;
信号掩码;
优先级。
B.2 基本线程函数:创建和终止
本节讨论5个基本线程函数。
B.2.1 pthread_create函数
当一个程序由exec启动执行时,系统将创建一个称为初始线程(initial thread)或主线程(main thread)的单个线程。其余线程则由pthread_create函数创建。
#include <pthread.h>
int pthread_create(pthread_t *tid,const pthread_attr_t *attr,
void *(*func)(void *),void *arg);
返回:若成功则为0,若出错则为正的Exxx值
一个进程内的各个线程是由线程ID(thread ID)标识的,这些线程的数据类型为pthread_t。如果新的线程创建成功,它的ID就通过tid指针返回。
每个线程有多个属性(attribute):优先级、初始栈大小、是否应该是一个守护线程,等等。当创建线程时,我们可通过初始化一个pthread_attr_t变量来指定这些属性以覆盖默认值。我们通常采用默认值,这种情况下,我们只需把attr参数指定为一个空指针。
最后,当创建一个线程时,我们应指定一个它将执行的函数,称为它的线程启动函数(thread start function)。这个线程以调用该函数开始,以后或者显式地终止(调用pthread_exit),或者隐式地终止(让该函数返回)。该函数的地址作为func参数指定,该函数唯一的调用参数则是一个指针arg。如果需要给该函数传递多个参数,我们就得把它们打包成一个结构,然后将其地址作为这个唯一的参数,传递给线程启动函数。
注意func和arg的声明。func函数接受一个通用指针参数(void *),返回一个通用指针(void *)。这就使得我们可以给线程传递一个指针(指向任何我们想要指向的东西),再由线程返回一个指针(同样地指向任何我们想要指向的东西)。
Pthread函数的返回值有两种:成功时返回0,出错时返回非零。与出错时返回-1,并置errno为某个正值的大多数系统函数不同,Pthread函数的返回值是正值的出错指示。例如,如果pthread_create函数因为超过了系统线程数目的限制而不能创建新线程,那么它的返回值将是EAGAIN。Pthread函数并不设置errno。成功时返回0,出错时返回非零的约定不成问题,因为在<sys/errno.h>头文件中的所有Exxx值都大于0。0值永远不会赋给任何一个Exxx常值。
B.2.2 pthread_join函数
我们可以调用pthread_join等待一个线程终止。把线程和Unix进程相比, pthread_create类似于fork,pthread_join则类似于waitpid。
#include <pthread.h>
int pthread_join(pthread_t tid,void **status);
返回:若成功则为0,若出错则为正的Exxx值
我们必须指定要等待的线程的tid。遗憾的是,不是任意一个线程的终止都能等待(类似于给waitpid的进程ID参数传递−1值的情况)。
如果status指针非空,那么所等待线程的返回值(指向某个对象的指针)将存放在status指向的位置。
B.2.3 pthread_self函数
每个线程都有在某个给定的进程内标识自身的一个ID。这个线程ID由pthread_create函数返回,我们已看到pthread_join函数用到它。一个线程使用pthread_self取得自己的线程ID。
#include <pthread.h>
pthread_t pthread_self(void);
返回:调用线程的线程ID
把线程和Unix进程相比较,pthread_self类似于getpid。
B.2.4 pthread_detach函数
线程或者是可汇合的(joinable),或者是脱离的(detached)。当可汇合的线程终止时,其线程ID和退出状态将保留,直到另外一个线程调用pthread_join。脱离的线程则像守护进程:当它终止时,所有的资源都将释放,因此我们不能等待它终止。如果一个线程需要知道另一个线程的终止时间,那就最好保留第二个线程的可汇合性。
pthread_detach函数将指定的线程变为脱离的。
#include <pthread.h>
int pthread_detach(pthread_t tid);
返回:若成功则为0,若出错则为正的Exxx值
该函数通常由想让自己脱离的线程使用,例如:
pthread_detach(pthread_self());
B.2.5 pthread_exit函数
终止一个线程的方法之一是调用pthread_exit。
#include <pthread.h>
void pthread_exit(void *status);
不返回到调用者
如果该线程未脱离,那么其线程ID和退出状态将一直保留到调用进程内的另外某个线程调用pthread_join为止。
指针status不能指向局部于调用线程的对象(例如该线程的启动函数中的某个自动变量),因为该线程终止时那个对象也消失了。
使一个线程终止还有其他两种方法。
启动该线程的函数(pthread_create的第三个参数)可以调用return。既然该函数必须声明成返回一个void指针,该返回值便是该线程的终止状态。
如果本进程的main函数返回,或者某个线程调用了exit或_exit,那么该进程将立即终
止,它的仍在运行的任意线程也都将终止。
[1]. 线程和轻权进程实际上是不同的概念。Solaris 2.x下实际存在内核线程、轻权进程和用户线程三个概念。——译者注