/* example.c*/
#include <stdio.h>
#include <pthread.h>
void thread(void)
{
int i;
for(i=0;i<3;i++)
printf("This is a pthread.\n");
}
int main(void)
{
pthread_t id;
int i,ret;
ret=pthread_create(&id,NULL,(void *) thread,NULL);
if(ret!=0){
printf ("Create pthread error!\n");
exit (1);
}
for(i=0;i<3;i++)
printf("This is the main process.\n");
pthread_join(id,NULL);
return (0);
}
我们~译此程序:
gcc example1.c -lpthread -o example1
q行example1Q我们得到如下结果:
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.
再次q行Q我们可能得到如下结果:
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.
前后两次l果不一Pq是两个U程争夺CPU资源的结果。上面的CZ中,我们使用C两个函数Q pthread_create和pthread_joinQƈ声明了一个pthread_t型的变量?br> pthread_t在头文g/usr/include/bits/pthreadtypes.h中定义:
typedef unsigned long int pthread_t;
它是一个线E的标识W。函数pthread_create用来创徏一个线E,它的原型为:
extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr,
void *(*__start_routine) (void *), void *__arg));
W一个参Cؓ指向U程标识W的指针Q第二个参数用来讄U程属性,W三个参数是U程q行函数的v始地址Q最后一个参数是q行函数的参数。这里,我们的函数thread不需要参敎ͼ所以最后一个参数设为空指针。第二个参数我们也设为空指针Q这样将生成默认属性的U程。对U程属性的讑֮和修Ҏ们将在下一节阐q。当创徏U程成功Ӟ函数q回0Q若不ؓ0则说明创建线E失败,常见的错误返回代码ؓEAGAIN和EINVAL。前者表C系l限制创建新的线E,例如U程数目q多了;后者表C第二个参数代表的线E属性值非法。创建线E成功后Q新创徏的线E则q行参数三和参数四确定的函数Q原来的U程则l运行下一行代码?br> 函数pthread_join用来{待一个线E的l束。函数原型ؓQ?br> extern int pthread_join __P ((pthread_t __th, void **__thread_return));
W一个参Cؓ被等待的U程标识W,W二个参Cؓ一个用户定义的指针Q它可以用来存储被等待线E的q回倹{这个函数是一个线E阻塞的函数Q调用它的函数将一直等待到被等待的U程l束为止Q当函数q回Ӟ被等待线E的资源被收回。一个线E的l束有两U途径Q一U是象我们上面的例子一P函数l束了,调用它的U程也就l束了;另一U方式是通过函数pthread_exit来实现。它的函数原型ؓQ?br> extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));
唯一的参数是函数的返回代码,只要pthread_join中的W二个参数thread_return不是NULLQ这个值将被传递给thread_return。最后要说明的是Q一个线E不能被多个U程{待Q否则第一个接收到信号的线E成功返回,其余调用pthread_join的线E则q回错误代码ESRCH?br> 在这一节里Q我们编写了一个最单的U程Qƈ掌握了最常用的三个函数pthread_createQpthread_join和pthread_exit。下面,我们来了解线E的一些常用属性以及如何设|这些属性?/p>
3 修改U程的属?br> 在上一节的例子里,我们用pthread_create函数创徏了一个线E,在这个线E中Q我们用了默认参数Q即该函数的第二个参数设ؓNULL。的,对大多数E序来说Q用默认属性就够了Q但我们q是有必要来了解一下线E的有关属性?br> 属性结构ؓpthread_attr_tQ它同样在头文g/usr/include/pthread.h中定义,喜欢q根问底的h可以自己L看。属性g能直接设|,M用相兛_数进行操作,初始化的函数为pthread_attr_initQ这个函数必dpthread_create函数之前调用。属性对象主要包括是否绑定、是否分R堆栈地址、堆栈大、优先。默认的属性ؓ非绑定、非分离、缺?M的堆栈、与父进E同L别的优先U?br> 关于U程的绑定,牉|到另外一个概念:轻进E(LWPQLight Weight ProcessQ。轻q程可以理解为内核线E,它位于用户层和系l层之间。系l对U程资源的分配、对U程的控制是通过轻进E来实现的,一个轻q程可以控制一个或多个U程。默认状况下Q启动多轻q程、哪些轻q程来控制哪些线E是ql来控制的,q种状况即称为非l定的。绑定状况下Q则思义Q即某个U程固定?l?在一个轻q程之上。被l定的线E具有较高的响应速度Q这是因为CPU旉片的调度是面向轻q程的,l定的线E可以保证在需要的时候它L一个轻q程可用。通过讄被绑定的轻进E的优先U和调度U可以得绑定的U程满诸如实时反应之类的要求?br> 讄U程l定状态的函数为pthread_attr_setscopeQ它有两个参敎ͼW一个是指向属性结构的指针Q第二个是绑定类型,它有两个取|PTHREAD_SCOPE_SYSTEMQ绑定的Q和PTHREAD_SCOPE_PROCESSQ非l定的)。下面的代码卛_Z一个绑定的U程?br>#include <pthread.h>
pthread_attr_t attr;
pthread_t tid;
/*初始化属性|均设为默认?/
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
pthread_create(&tid, &attr, (void *) my_function, NULL);
U程的分ȝ态决定一个线E以什么样的方式来l止自己。在上面的例子中Q我们采用了U程的默认属性,即ؓ非分ȝ态,q种情况下,原有的线E等待创建的U程l束。只有当pthread_joinQ)函数q回Ӟ创徏的线E才终止,才能释放自己占用的系l资源。而分ȝE不是这样子的,它没有被其他的线E所{待Q自p行结束了Q线E也q止了Q马上释攄l资源。程序员应该Ҏ自己的需要,选择适当的分ȝ态。设|线E分ȝ态的函数为pthread_attr_setdetachstateQpthread_attr_t *attr, int detachstateQ。第二个参数可选ؓPTHREAD_CREATE_DETACHEDQ分ȝE)?PTHREAD _CREATE_JOINABLEQ非分离U程Q。这里要注意的一ҎQ如果设|一个线Eؓ分离U程Q而这个线E运行又非常快,它很可能在pthread_create函数q回之前q止了Q它l止以后可能将U程号和pȝ资源UMl其他的U程使用Q这栯用pthread_create的线E就得到了错误的U程受要避免q种情况可以采取一定的同步措施Q最单的Ҏ之一是可以在被创建的U程里调用pthread_cond_timewait函数Q让q个U程{待一会儿Q留够的旉让函数pthread_createq回。设|一D늭待时_是在多线E编E里常用的方法。但是注意不要用诸如waitQ)之类的函敎ͼ它们是整个q程睡眠Qƈ不能解决U程同步的问题?br> 另外一个可能常用的属性是U程的优先Q它存放在结构sched_param中。用函数pthread_attr_getschedparam和函数pthread_attr_setschedparamq行存放Q一般说来,我们L先取优先U,对取得的g改后再存攑֛厅R下面即是一D늮单的例子?br>#include <pthread.h>
#include <sched.h>
pthread_attr_t attr;
pthread_t tid;
sched_param param;
int newprio=20;
pthread_attr_init(&attr);
pthread_attr_getschedparam(&attr, ¶m);
param.sched_priority=newprio;
pthread_attr_setschedparam(&attr, ¶m);
pthread_create(&tid, &attr, (void *)myfunction, myarg);
4 U程的数据处?br> 和进E相比,U程的最大优点之一是数据的׃n性,各个q程׃n父进E处沿袭的数据段Q可以方便的获得、修Ҏ据。但q也l多U程~程带来了许多问题。我们必d心有多个不同的进E访问相同的变量。许多函数是不可重入的,卛_时不能运行一个函数的多个拯Q除非用不同的数据D)。在函数中声明的静态变量常常带来问题,函数的返回g会有问题。因为如果返回的是函数内部静态声明的I间的地址Q则在一个线E调用该函数得到地址后用该地址指向的数据时Q别的线E可能调用此函数q修改了q一D|据。在q程中共享的变量必须用关键字volatile来定义,q是Z防止~译器在优化Ӟ如gcc中?OX参数Q改变它们的使用方式。ؓ了保护变量,我们必须使用信号量、互斥等Ҏ来保证我们对变量的正用。下面,我们逐步介绍处理U程数据时的有关知识?/p>
4.1 U程数据
在单U程的程序里Q有两种基本的数据:全局变量和局部变量。但在多U程E序里,q有W三U数据类型:U程数据QTSD: Thread-Specific DataQ。它和全局变量很象Q在U程内部Q各个函数可以象使用全局变量一栯用它Q但它对U程外部的其它线E是不可见的。这U数据的必要性是显而易见的。例如我们常见的变量errnoQ它q回标准的出错信息。它昄不能是一个局部变量,几乎每个函数都应该可以调用它Q但它又不能是一个全局变量Q否则在AU程里输出的很可能是BU程的出错信息。要实现诸如此类的变量,我们必M用线E数据。我们ؓ每个U程数据创徏一个键Q它和这个键相关联,在各个线E里Q都使用q个键来指代U程数据Q但在不同的U程里,q个键代表的数据是不同的Q在同一个线E里Q它代表同样的数据内宏V?br> 和线E数据相关的函数主要?个:创徏一个键Qؓ一个键指定U程数据Q从一个键dU程数据Q删除键?br> 创徏键的函数原型为:
extern int pthread_key_create __P ((pthread_key_t *__key,
void (*__destr_function) (void *)));
W一个参Cؓ指向一个键值的指针Q第二个参数指明了一个destructor函数Q如果这个参C为空Q那么当每个U程l束Ӟpȝ调用这个函数来释放l定在这个键上的内存块。这个函数常和函数pthread_once ((pthread_once_t*once_control, void (*initroutine) (void)))一起用,Z让这个键只被创徏一ơ。函数pthread_once声明一个初始化函数Q第一ơ调用pthread_once时它执行q个函数Q以后的调用被它忽略?br>
在下面的例子中,我们创徏一个键Qƈ它和某个数据相兌。我们要定义一个函数createWindowQ这个函数定义一个图形窗口(数据cd为Fl_Window *Q这是图形界面开发工具FLTK中的数据cdQ。由于各个线E都会调用这个函敎ͼ所以我们用线E数据?br>/* 声明一个键*/
pthread_key_t myWinKey;
/* 函数 createWindow */
void createWindow ( void ) {
Fl_Window * win;
static pthread_once_t once= PTHREAD_ONCE_INIT;
/* 调用函数createMyKeyQ创建键*/
pthread_once ( & once, createMyKey) ;
/*win指向一个新建立的窗?/
win=new Fl_Window( 0, 0, 100, 100, "MyWindow");
/* ҎH口作一些可能的讄工作Q如大小、位|、名U等*/
setWindow(win);
/* 窗口指针值绑定在键myWinKey?/
pthread_setpecific ( myWinKey, win);
}
/* 函数 createMyKeyQ创Z个键Qƈ指定了destructor */
void createMyKey ( void ) {
pthread_keycreate(&myWinKey, freeWinKey);
}
/* 函数 freeWinKeyQ释攄?/
void freeWinKey ( Fl_Window * win){
delete win;
}
q样Q在不同的线E中调用函数createMyWinQ都可以得到在线E内部均可见的窗口变量,q个变量通过函数pthread_getspecific得到。在上面的例子中Q我们已l用了函数pthread_setspecific来将U程数据和一个键l定在一赗这两个函数的原型如下:
extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer));
extern void *pthread_getspecific __P ((pthread_key_t __key));
q两个函数的参数意义和用方法是显而易见的。要注意的是Q用pthread_setspecificZ个键指定新的U程数据Ӟ必须自己释放原有的线E数据以回收I间。这个过E函数pthread_key_delete用来删除一个键Q这个键占用的内存将被释放,但同栯注意的是Q它只释N占用的内存,q不释放该键兌的线E数据所占用的内存资源,而且它也不会触发函数pthread_key_create中定义的destructor函数。线E数据的释放必须在释N之前完成?/p>
4.2 互斥?br> 互斥锁用来保证一D|间内只有一个线E在执行一D代码。必要性显而易见:假设各个U程向同一个文仉序写入数据,最后得到的l果一定是N性的?br> 我们先看下面一D代码。这是一个读/写程序,它们公用一个缓冲区Qƈ且我们假定一个缓冲区只能保存一条信息。即~冲区只有两个状态:有信息或没有信息?/p>
void reader_function ( void );
void writer_function ( void );
char buffer;
int buffer_has_item=0;
pthread_mutex_t mutex;
struct timespec delay;
void main ( void ){
pthread_t reader;
/* 定义延迟旉*/
delay.tv_sec = 2;
delay.tv_nec = 0;
/* 用默认属性初始化一个互斥锁对象*/
pthread_mutex_init (&mutex,NULL);
pthread_create(&reader, pthread_attr_default, (void *)&reader_function), NULL);
writer_function( );
}
void writer_function (void){
while(1){
/* 锁定互斥?/
pthread_mutex_lock (&mutex);
if (buffer_has_item==0){
buffer=make_new_item( );
buffer_has_item=1;
}
/* 打开互斥?/
pthread_mutex_unlock(&mutex);
pthread_delay_np(&delay);
}
}
void reader_function(void){
while(1){
pthread_mutex_lock(&mutex);
if(buffer_has_item==1){
consume_item(buffer);
buffer_has_item=0;
}
pthread_mutex_unlock(&mutex);
pthread_delay_np(&delay);
}
}
q里声明了互斥锁变量mutexQ结构pthread_mutex_tZ公开的数据类型,其中包含一个系l分配的属性对象。函数pthread_mutex_init用来生成一个互斥锁。NULL参数表明使用默认属性。如果需要声明特定属性的互斥锁,调用函数pthread_mutexattr_init。函数pthread_mutexattr_setpshared和函数pthread_mutexattr_settype用来讄互斥锁属性。前一个函数设|属性psharedQ它有两个取|PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED。前者用来不同进E中的线E同步,后者用于同步本q程的不同线E。在上面的例子中Q我们用的是默认属性PTHREAD_PROCESS_ PRIVATE。后者用来设|互斥锁cdQ可选的cd有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE和PTHREAD _MUTEX_DEFAULT。它们分别定义了不同的上所、解锁机Ӟ一般情况下Q选用最后一个默认属性?br> pthread_mutex_lock声明开始用互斥锁上锁,此后的代码直臌用pthread_mutex_unlock为止Q均被上锁,卛_一旉只能被一个线E调用执行。当一个线E执行到pthread_mutex_lock处时Q如果该锁此时被另一个线E用,那此U程被阻塞,即程序将{待到另一个线E释放此互斥锁。在上面的例子中Q我们用了pthread_delay_np函数Q让U程睡眠一D|_是Z防止一个线E始l占据此函数?br> 上面的例子非常简单,׃再介l了Q需要提出的是在使用互斥锁的q程中很有可能会出现死锁Q两个线E试囑时占用两个资源,q按不同的次序锁定相应的互斥锁,例如两个U程都需要锁定互斥锁1和互斥锁2QaU程先锁定互斥锁1QbU程先锁定互斥锁2Q这时就出现了死锁。此时我们可以用函数pthread_mutex_trylockQ它是函数pthread_mutex_lock的非d版本Q当它发现死锁不可避免时Q它会返回相应的信息Q程序员可以针对死锁做出相应的处理。另外不同的互斥锁类型对死锁的处理不一P但最主要的还是要E序员自己在E序设计注意q一炏V?/p>
4.3 条g变量
前一节中我们讲述了如何用互斥锁来实现线E间数据的共享和通信Q互斥锁一个明昄~点是它只有两种状态:锁定和非锁定。而条件变量通过允许U程d和等待另一个线E发送信LҎ弥补了互斥锁的不I它常和互斥锁一起用。用时Q条件变量被用来d一个线E,当条件不满ӞU程往往解开相应的互斥锁q等待条件发生变化。一旦其它的某个U程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的U程。这些线E将重新锁定互斥锁ƈ重新试条g是否满。一般说来,条g变量被用来进行线扉K的同步?br> 条g变量的结构ؓpthread_cond_tQ函数pthread_cond_initQ)被用来初始化一个条件变量。它的原型ؓQ?br> extern int pthread_cond_init __P ((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr));
其中cond是一个指向结构pthread_cond_t的指针,cond_attr是一个指向结构pthread_condattr_t的指针。结构pthread_condattr_t是条件变量的属性结构,和互斥锁一h们可以用它来讄条g变量是进E内可用q是q程间可用,默认值是PTHREAD_ PROCESS_PRIVATEQ即此条件变量被同一q程内的各个U程使用。注意初始化条g变量只有未被使用时才能重新初始化或被释放。释放一个条件变量的函数为pthread_cond_ destroyQpthread_cond_t condQ。
函数pthread_cond_waitQ)使线E阻塞在一个条件变量上。它的函数原型ؓQ?br> extern int pthread_cond_wait __P ((pthread_cond_t *__cond,
pthread_mutex_t *__mutex));
U程解开mutex指向的锁q被条g变量condd。线E可以被函数pthread_cond_signal和函数pthread_cond_broadcast唤醒Q但是要注意的是Q条件变量只是vd和唤醒线E的作用Q具体的判断条gq需用户l出Q例如一个变量是否ؓ0{等Q这一Ҏ们从后面的例子中可以看到。线E被唤醒后,它将重新查判断条件是否满I如果q不满Q一般说来线E应该仍d在这里,被等待被下一ơ唤醒。这个过E一般用while语句实现?br> 另一个用来阻塞线E的函数是pthread_cond_timedwaitQ)Q它的原型ؓQ?br> extern int pthread_cond_timedwait __P ((pthread_cond_t *__cond,
pthread_mutex_t *__mutex, __const struct timespec *__abstime));
它比函数pthread_cond_waitQ)多了一个时间参敎ͼl历abstimeD|间后Q即使条件变量不满Q阻塞也被解除?br> 函数pthread_cond_signalQ)的原型ؓQ?br> extern int pthread_cond_signal __P ((pthread_cond_t *__cond));
它用来释放被d在条件变量cond上的一个线E。多个线E阻塞在此条件变量上Ӟ哪一个线E被唤醒是由U程的调度策略所军_的。要注意的是Q必ȝ保护条g变量的互斥锁来保护这个函敎ͼ否则条g满信号又可能在试条g和调用pthread_cond_wait函数之间被发出,从而造成无限制的{待。下面是使用函数pthread_cond_waitQ)和函数pthread_cond_signalQ)的一个简单的例子?/p>
pthread_mutex_t count_lock;
pthread_cond_t count_nonzero;
unsigned count;
decrement_count () {
pthread_mutex_lock (&count_lock);
while(count==0)
pthread_cond_wait( &count_nonzero, &count_lock);
count=count -1;
pthread_mutex_unlock (&count_lock);
}
increment_count(){
pthread_mutex_lock(&count_lock);
if(count==0)
pthread_cond_signal(&count_nonzero);
count=count+1;
pthread_mutex_unlock(&count_lock);
}
countgؓ0Ӟdecrement函数在pthread_cond_wait处被dQƈ打开互斥锁count_lock。此Ӟ当调用到函数increment_countӞpthread_cond_signalQ)函数改变条g变量Q告知decrement_countQ)停止d。读者可以试着让两个线E分别运行这两个函数Q看看会出现什么样的结果?br> 函数pthread_cond_broadcastQpthread_cond_t *condQ用来唤醒所有被d在条件变量cond上的U程。这些线E被唤醒后将再次竞争相应的互斥锁Q所以必d心用这个函数?/p>
4.4 信号?br> 信号量本质上是一个非负的整数计数器,它被用来控制对公p源的讉K。当公共资源增加Ӟ调用函数sem_postQ)增加信号量。只有当信号量值大于0Ӟ才能使用公共资源Q用后Q函数sem_waitQ)减少信号量。函数sem_trywaitQ)和函数pthread_ mutex_trylockQ)起同L作用Q它是函数sem_waitQ)的非d版本。下面我们逐个介绍和信号量有关的一些函敎ͼ它们都在头文?usr/include/semaphore.h中定义?br> 信号量的数据cd为结构sem_tQ它本质上是一个长整型的数。函数sem_initQ)用来初始化一个信号量。它的原型ؓQ?br> extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));
sem为指向信号量l构的一个指针;pshared不ؓQ时此信号量在进E间׃nQ否则只能ؓ当前q程的所有线E共享;valuel出了信号量的初始倹{?br> 函数sem_post( sem_t *sem )用来增加信号量的倹{当有线E阻塞在q个信号量上Ӟ调用q个函数会其中的一个线E不在阻塞,选择机制同样是由U程的调度策略决定的?br> 函数sem_wait( sem_t *sem )被用来阻塞当前线E直C号量sem的值大?Q解除阻塞后sem的值减一Q表明公p源经使用后减。函数sem_trywait ( sem_t *sem )是函数sem_waitQ)的非d版本Q它直接信号量sem的值减一?br> 函数sem_destroy(sem_t *sem)用来释放信号量sem?br> 下面我们来看一个用信号量的例子。在q个例子中,一共有4个线E,其中两个U程负责从文件读取数据到公共的缓冲区Q另两个U程从缓冲区d数据作不同的处理Q加和乘q算Q?br>/* File sem.c */
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#define MAXSTACK 100
int stack[MAXSTACK][2];
int size=0;
sem_t sem;
/* 从文?.datd数据Q每Mơ,信号量加一*/
void ReadData1(void){
FILE *fp=fopen("1.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
sem_post(&sem);
++size;
}
fclose(fp);
}
/*从文?.datd数据*/
void ReadData2(void){
FILE *fp=fopen("2.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
sem_post(&sem);
++size;
}
fclose(fp);
}
/*d{待~冲区有数据Q读取数据后Q释攄_l箋{待*/
void HandleData1(void){
while(1){
sem_wait(&sem);
printf("Plus:%d+%d=%d\n",stack[size][0],stack[size][1],
stack[size][0]+stack[size][1]);
--size;
}
}
void HandleData2(void){
while(1){
sem_wait(&sem);
printf("Multiply:%d*%d=%d\n",stack[size][0],stack[size][1],
stack[size][0]*stack[size][1]);
--size;
}
}
int main(void){
pthread_t t1,t2,t3,t4;
sem_init(&sem,0,0);
pthread_create(&t1,NULL,(void *)HandleData1,NULL);
pthread_create(&t2,NULL,(void *)HandleData2,NULL);
pthread_create(&t3,NULL,(void *)ReadData1,NULL);
pthread_create(&t4,NULL,(void *)ReadData2,NULL);
/* 防止E序q早退出,让它在此无限期等?/
pthread_join(t1,NULL);
}
在Linux下,我们用命令gcc -lpthread sem.c -o sem生成可执行文件sem?我们事先~辑好数据文?.dat?.datQ假讑֮们的内容分别? 2 3 4 5 6 7 8 9 10?-1 -2 -3 -4 -5 -6 -7 -8 -9 -10 Q我们运行semQ得到如下的l果Q?br>Multiply:-1*-2=2
Plus:-1+-2=-3
Multiply:9*10=90
Plus:-9+-10=-19
Multiply:-7*-8=56
Plus:-5+-6=-11
Multiply:-3*-4=12
Plus:9+10=19
Plus:7+8=15
Plus:5+6=11
从中我们可以看出各个U程间的竞争关系。而数值ƈ未按我们原先的顺序显C出来这是由于sizeq个数D各个U程L修改的缘故。这也往往是多U程~程要注意的问题?/p>
5 结
多线E编E是一个很有意思也很有用的技术,使用多线E技术的|络蚂蚁是目前最常用的下载工具之一Q用多U程技术的grep比单U程的grep要快上几倍,cM的例子还有很多。希望大家能用多U程技术写出高效实用的好程序来?/p>