Linux 下的多线程编程 
本文出自:http://www.china-pub.com 作者: 姚继锋 (2001-08-11 09:05:00) 
言 
线程(thread)技术早在 60 年代就被提出,但真正应用多线程到操作系统中去,是在 80 年代中
solaris 是这方面的佼佼者。传统的 Unix 也支持线程的概念,但是在一个进程(process)中只
有一个线程,这样多线程就意味着多进程。现在,多线程技术已经被许多操作系统所支持,包括
dows/NT,当然,也包括 Linux。 
为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选
线程?我们首先必须回答这些问题。 
使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在
ux 系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码
堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼
间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所
的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的
,一个进程的开销大约是一个线程开销的 30 倍左右,当然,在具体的系统上,这个数据可能会
大的区别。 
使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,
行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于
进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,
方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程
声明为 static 的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最
注意的地方。 
除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有
的优点: 
1) 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都
待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作
ime consuming)置于一个新的线程,可以避免这种尴尬的情况。 
2) 使多 CPU 系统更加有效。操作系统会保证当线程数不大于 CPU 数目时,不同的线程运行于不
CPU 上。 
3) 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运
分,这样的程序会利于理解和修改。 
下面我们先来尝试编写一个简单的多线程程序。 
1 引
   
期,
允许
Win
   
用多
   
Lin
段、
此之
花费
说来
有较
   
要进
同一
而且
序中
需要
   
以下
   
会等
(t
   
同的
   
行部
   
 
2 简
   
使用
通过
关于
线程
单的多线程编程 
Linux 系统下的多线程遵循 POSIX 线程接口,称为 pthread。编写 Linux 下的多线程程序,需要
头文件 pthread.h,连接时需要使用库 libpthread.a。顺便说一下,Linux 下 pthread 的实现是
系统调用 clone()来实现的。clone()是 Linux 所特有的系统调用,它的使用方式类似 fork,
clone()的详细情况,有兴趣的读者可以去查看有关文档说明。下面我们展示一个最简单的多
程序 example1.c。  
/* e
xample.c*/ 
#inc
lude  
#inc
lude 
 
void
 thread(void) 
{ 
int 
i; 
for(
i=0;i<3;i++) 
prin
tf("This is a pthread.\n"); 
} 
int 
main(void) 
{ 
pthr
ead_t id; 
int 
i,ret; 
ret=
pthread_create(&id,NULL,(void *) thread,NULL); 
if(r
et!=0){ 
prin
tf ("Create pthread error!\n"); 
exit
 (1); 
} 
for(
i=0;i<3;i++) 
prin
tf("This is the main process.\n"); 
pthr
ead_join(id,NULL); 
retu
rn (0); 
} 
我们
编译此程序: 
gcc 
example1.c -lpthread -o example1 
运行
example1,我们得到如下结果: 
This
 is the main process. 
This
 is a pthread. 
This
 is the main process. 
This
 is the main process. 
This
 is a pthread. 
This
 is a pthread. 
再次
运行,我们可能得到如下结果: 
This
 is a pthread. 
This
 is the main process. 
This
 is a pthread. 
This
 is the main process. 
This
 is a pthread. 
This
 is the main process. 
 
   
前后两次结果不一样,这是两个线程争夺 CPU 资源的结果。上面的示例中,我们使用到了两个函数,    pthread_create 和
pthr
ead_join,并声明了一个 pthread_t 型的变量。 
   
pthread_t 在头文件/usr/include/bits/pthreadtypes.h 中定义: 
   
typedef unsigned long int pthread_t; 
   
它是一个线程的标识符。函数 pthread_create 用来创建一个线程,它的原型为: 
   
extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr, 
   
void *(*__start_routine) (void *), void *__arg)); 
   
第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是
运行
函数的参数。这里,我们的函数 thread 不需要参数,所以最后一个参数设为空指针。第二个参数我们也设为空指针,这样将生成
默认
属性的线程。对线程属性的设定和修改我们将在下一节阐述。当创建线程成功时,函数返回 0,若不为 0 则说明创建线程失败,常
见的
错误返回代码为 EAGAIN 和 EINVAL。前者表示系统限制创建新的线程,例如线程数目过多了;后者表示第二个参数代表的线程属性
值非
法。创建线程成功后,新创建的线程则运行参数三和参数四确定的函数,原来的线程则继续运行下一行代码。 
   
函数 pthread_join 用来等待一个线程的结束。函数原型为: 
   
extern int pthread_join __P ((pthread_t __th, void **__thread_return)); 
   
第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。这个函数是一个
线程
阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。一个线程的结束有
两种
途径,一种是象我们上面的例子一样,函数结束了,调用它的线程也就结束了;另一种方式是通过函数 pthread_exit 来实现。它
的函
数原型为: 
   
extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__)); 
   
唯一的参数是函数的返回代码,只要 pthread_join 中的第二个参数 thread_return 不是 NULL,这个值将被传递给 thread_return。
最后
要说明的是,一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用 pthread_join 的线程则返回错误
代码
ESRCH。 
   
在这一节里,我们编写了一个最简单的线程,并掌握了最常用的三个函数 pthread_create,pthread_join 和 pthread_exit。下面,
我们
来了解线程的一些常用属性以及如何设置这些属性。 
3 修
改线程的属性 
   
在上一节的例子里,我们用 pthread_create 函数创建了一个线程,在这个线程中,我们使用了默认参数,即将该函数的第二个参
数设
为 NULL。的确,对大多数程序来说,使用默认属性就够了,但我们还是有必要来了解一下线程的有关属性。 
   
属性结构为 pthread_attr_t,它同样在头文件/usr/include/pthread.h 中定义,喜欢追根问底的人可以自己去查看。属性值不能
直接
设置,须使用相关函数进行操作,初始化的函数为 pthread_attr_init,这个函数必须在 pthread_create 函数之前调用。属性对
象主
要包括是否绑定、是否分离、堆栈地址、堆栈大小、优先级。默认的属性为非绑定、非分离、缺省 1M 的堆栈、与父进程同样级别
的优
先级。 
   
关于线程的绑定,牵涉到另外一个概念:轻进程(LWP:Light Weight Process)。轻进程可以理解为内核线程,它位于用户层和
系统
层之间。系统对线程资源的分配、对线程的控制是通过轻进程来实现的,一个轻进程可以控制一个或多个线程。默认状况下,启
动多
少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定的。绑定状况下,则顾名思义,即某个线程固
定的
"绑"在一个轻进程之上。被绑定的线程具有较高的响应速度,这是因为 CPU 时间片的调度是面向轻进程的,绑定的线程可以保证
在需
要的时候它总有一个轻进程可用。通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求。
   
设置线程绑定状态的函数为 pthread_attr_setscope,它有两个参数,第一个是指向属性结构的指针,第二个是绑定类型,它有两
个取
值:PTHREAD_SCOPE_SYSTEM(绑定的)和 PTHREAD_SCOPE_PROCESS(非绑定的)。下面的代码即创建了一个绑定的线程。 
#inc
lude 
 
pthr
ead_attr_t attr; 
pthr
ead_t tid; 
/*初
始化属性值,均设为默认值*/ 
pthr
ead_attr_init(&attr); 
pthr
ead_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM); 
pthr
ead_create(&tid, &attr, (void *) my_function, NULL); 
   
线程的分离状态决定一个线程以什么样的方式来终止自己。在上面的例子中,我们采用了线程的默认属性,即为非分离状态,这
种情
况下,原有的线程等待创建的线程结束。只有当 pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统
资源
。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。程序员应该
根据
自己的需要,选择适当的分离状态。设置线程分离状态的函数为 pthread_attr_setdetachstate(pthread_attr_t *attr, int 
deta
chstate)。第二个参数可选为 PTHREAD_CREATE_DETACHED(分离线程)和 PTHREAD _CREATE_JOINABLE(非分离线程)。这里要注
意的
一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在 pthread_create 函数返回之前就终止了,它终止
以后
就可能将线程号和系统资源移交给其他的线程使用,这样调用 pthread_create 的线程就得到了错误的线程号。要避免这种情况可
以采
取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用 pthread_cond_timewait 函数,让这个线程等待一会儿,留
出足
够的时间让函数 pthread_create 返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如 wait()之类
的函
数,它们是使整个进程睡眠,并不能解决线程同步的问题。 
   
另外一个可能常用的属性是线程的优先级,它存放在结构 sched_param 中。用函数 pthread_attr_getschedparam 和函数
pthr
ead_attr_setschedparam 进行存放,一般说来,我们总是先取优先级,对取得的值修改后再存放回去。下面即是一段简单的例子。
#inc
lude 
 
#inc
lude  
pthr
ead_attr_t attr; 
pthr
ead_t tid; 
sche
d_param param; 
int 
newprio=20; 
pthr
ead_attr_init(&attr); 
pthr
ead_attr_getschedparam(&attr, ¶m); 
para
m.sched_priority=newprio; 
pthr
ead_attr_setschedparam(&attr, ¶m); 
pthr
ead_create(&tid, &attr, (void *)myfunction, myarg); 
   
 
4 线
程的数据处理 
   
和进程相比,线程的最大优点之一是数据的共享性,各个进程共享父进程处沿袭的数据段,可以方便的获得、修改数据。但这也
给多
线程编程带来了许多问题。我们必须当心有多个不同的进程访问相同的变量。许多函数是不可重入的,即同时不能运行一个函数
的多
个拷贝(除非使用不同的数据段)。在函数中声明的静态变量常常带来问题,函数的返回值也会有问题。因为如果返回的是函数
内部
静态声明的空间的地址,则在一个线程调用该函数得到地址后使用该地址指向的数据时,别的线程可能调用此函数并修改了这一
段数
据。在进程中共享的变量必须用关键字 volatile 来定义,这是为了防止编译器在优化时(如 gcc 中使用-OX 参数)改变它们的使
用方
式。为了保护变量,我们必须使用信号量、互斥等方法来保证我们对变量的正确使用。下面,我们就逐步介绍处理线程数据时的
有关
知识。 
4.1 
线程数据 
   
在单线程的程序里,有两种基本的数据:全局变量和局部变量。但在多线程程序里,还有第三种数据类型:线程数据(TSD: 
Thre
ad-Specific Data)。它和全局变量很象,在线程内部,各个函数可以象使用全局变量一样调用它,但它对线程外部的其它线程
是不
可见的。这种数据的必要性是显而易见的。例如我们常见的变量 errno,它返回标准的出错信息。它显然不能是一个局部变量,几
乎每
个函数都应该可以调用它;但它又不能是一个全局变量,否则在 A 线程里输出的很可能是 B 线程的出错信息。要实现诸如此类的
变量
,我们就必须使用线程数据。我们为每个线程数据创建一个键,它和这个键相关联,在各个线程里,都使用这个键来指代线程数
据,
但在不同的线程里,这个键代表的数据是不同的,在同一个线程里,它代表同样的数据内容。 
   
和线程数据相关的函数主要有 4 个:创建一个键;为一个键指定线程数据;从一个键读取线程数据;删除键。 
   
创建键的函数原型为: 
   
extern int pthread_key_create __P ((pthread_key_t *__key, 
   
void (*__destr_function) (void *))); 
   
第一个参数为指向一个键值的指针,第二个参数指明了一个 destructor 函数,如果这个参数不为空,那么当每个线程结束时,系
统将
调用这个函数来释放绑定在这个键上的内存块。这个函数常和函数 pthread_once ((pthread_once_t*once_control, void 
(*in
itroutine) (void)))一起使用,为了让这个键只被创建一次。函数 pthread_once 声明一个初始化函数,第一次调用 pthread_once
时它
执行这个函数,以后的调用将被它忽略。 
 
   
在下面的例子中,我们创建一个键,并将它和某个数据相关联。我们要定义一个函数 createWindow,这个函数定义一个图形窗口
(数
据类型为 Fl_Window *,这是图形界面开发工具 FLTK 中的数据类型)。由于各个线程都会调用这个函数,所以我们使用线程数据。
/* 声
明一个键*/ 
pthr
ead_key_t myWinKey; 
/* 函
数 createWindow */ 
void
 createWindow ( void ) { 
Fl_W
indow * win; 
stat
ic pthread_once_t once= PTHREAD_ONCE_INIT; 
/* 调
用函数 createMyKey,创建键*/ 
pthr
ead_once ( & once, createMyKey) ; 
/*wi
n 指向一个新建立的窗口*/ 
win=
new Fl_Window( 0, 0, 100, 100, "MyWindow"); 
/* 对
此窗口作一些可能的设置工作,如大小、位置、名称等*/ 
setW
indow(win); 
/* 将
窗口指针值绑定在键 myWinKey 上*/ 
pthr
ead_setpecific ( myWinKey, win); 
} 
/* 函
数 createMyKey,创建一个键,并指定了 destructor */ 
void
 createMyKey ( void ) { 
pthr
ead_keycreate(&myWinKey, freeWinKey); 
} 
/* 函
数 freeWinKey,释放空间*/ 
void
 freeWinKey ( Fl_Window * win){ 
dele
te win; 
} 
   
这样,在不同的线程中调用函数 createMyWin,都可以得到在线程内部均可见的窗口变量,这个变量通过函数 pthread_getspecific
得到
。在上面的例子中,我们已经使用了函数 pthread_setspecific 来将线程数据和一个键绑定在一起。这两个函数的原型如下: 
   
extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer)); 
   
extern void *pthread_getspecific __P ((pthread_key_t __key)); 
   
这两个函数的参数意义和使用方法是显而易见的。要注意的是,用 pthread_setspecific 为一个键指定新的线程数据时,必须自
己释
放原有的线程数据以回收空间。这个过程函数 pthread_key_delete 用来删除一个键,这个键占用的内存将被释放,但同样要注意
的是
,它只释放键占用的内存,并不释放该键关联的线程数据所占用的内存资源,而且它也不会触发函数 pthread_key_create 中定义
的 d
estructor 函数。线程数据的释放必须在释放键之前完成。 
4.2 
互斥锁 
   
互斥锁用来保证一段时间内只有一个线程在执行一段代码。必要性显而易见:假设各个线程向同一个文件顺序写入数据,最后得
到的
结果一定是灾难性的。 
   
我们先看下面一段代码。这是一个读/写程序,它们公用一个缓冲区,并且我们假定一个缓冲区只能保存一条信息。即缓冲区只有
两个
状态:有信息或没有信息。 
void
 reader_function ( void ); 
void
 writer_function ( void );  
char
 buffer; 
int 
buffer_has_item=0; 
pthr
ead_mutex_t mutex; 
stru
ct timespec delay; 
void
 main ( void ){ 
pthr
ead_t reader; 
/* 定
义延迟时间*/ 
dela
y.tv_sec = 2; 
dela
y.tv_nec = 0; 
/* 用
默认属性初始化一个互斥锁对象*/ 
pthr
ead_mutex_init (&mutex,NULL); 
pthr
ead_create(&reader, pthread_attr_default, (void *)&reader_function), NULL); 
writ
er_function( ); 
} 
void
 writer_function (void){ 
whil
e(1){ 
/* 锁
定互斥锁*/ 
pthr
ead_mutex_lock (&mutex); 
if (
buffer_has_item==0){ 
buff
er=make_new_item( ); 
buff
er_has_item=1; 
} 
/* 打
开互斥锁*/ 
pthr
ead_mutex_unlock(&mutex); 
pthr
ead_delay_np(&delay);