分类: LINUX
2009-07-18 15:20:33
前言: Linux下线程的创建和基本的使用
先介绍什么是线程.我们编写的程序大多数可以看成是单线程的.就是程序是按照一定的顺序来执行.如果我们使用线程的话,程序就会在我们创建线成的地方分叉,变成两个"程序"在执行.粗略的看来好象和子进程差不多的,其实不然.子进程是通过拷贝父进程的地址空间来执行的.而线程是通过共享程序代码来执行的,讲的通俗一点就是线程的相同的代码会被执行几次.使用线程的好处是可以节省资源,由于线程是通过共享代码的,所以没有进程调度那么复杂.
1.线程的创建和使用
线程的创建是用下面的几个函数来实现的.
#includeint pthread_create(pthread_t *thread,pthread_attr_t *attr, void *(*start_routine)(void *),void *arg); void pthread_exit(void *retval); int pthread_join(pthread *thread,void **thread_return);
pthread_create创建一个线程,thread是用来表明创建线程的ID,attr指出线程创建时候的属性,我们用NULL来表明使用缺省属性.start_routine函数指针是线程创建成功後开始执行的函数,arg是这个函数的唯一一个参数.表明传递给start_routine的参数. pthrea d_exit函数和exit函数类似用来退出线程.这个函数结束线程,释放函数的资源,并在最後阻塞,直到其他线程使用pthread_join函数等待它.然後将*retval的值传递给**thread_ return.由于这个函数释放所以的函数资源,所以retval不能够指向函数的局部变量. pt hread_join和wait调用一样用来等待指定的线程.注意由于线程不同于进程.所以线程的退出一定要用pthread_exit而不是使用exit.否则整个程序会退出的. 下面我们使用一个实例来解释一下使用方法.在实践中,我们经常要备份一些文件.下面这个程序可以实现当前目录下的所有文件备份.备份後的後缀名为bak
#include#include #include #include #include #include #include #include #include #include #include #define BUFFER 512 struct copy_file { int infile; int outfile; }; void *copy(void *arg) { int infile,outfile; int bytes_read,bytes_write,*bytes_copy_p; char buffer[BUFFER],*buffer_p; struct copy_file *file=(struct copy_file *)arg; infile=file->infile; outfile=file->outfile; /* 因为线程退出时,所有的变量空间都要被释放,所以我们只好自己分配内存了 */ if((bytes_copy_p=(int *)malloc(sizeof(int)))==NULL) pthread_exit(NULL); bytes_read=bytes_write=0; *bytes_copy_p=0; /* 还记得怎么拷贝文件吗 */ while((bytes_read=read(infile,buffer,BUFFER))!=0) { if((bytes_read==-1)&&(errno!=EINTR))break; else if(bytes_read>0) { buffer_p=buffer; while((bytes_write=write(outfile,buffer_p,bytes_read))!=0) { if((bytes_write==-1)&&(errno!=EINTR))break; else if(bytes_write==bytes_read)break; else if(bytes_write>0) { buffer_p+=bytes_write; bytes_read-=bytes_write; } } if(bytes_write==-1)break; *bytes_copy_p+=bytes_read; } } close(infile); close(outfile); pthread_exit(bytes_copy_p); } int main(int argc,char **argv) { pthread_t *thread; struct copy_file *file; int byte_copy,*byte_copy_p,num,i,j; char filename[BUFFER]; struct dirent **namelist; struct stat filestat; /* 得到当前路径下面所有的文件(包含目录)的个数 */ if((num=scandir(".",&namelist,0,alphasort))<0) { fprintf(stderr,"Get File Num Error:%s\n\a",strerror(errno)); exit(1); } /* 给线程分配空间,其实没有必要这么多的 */ if(((thread=(pthread_t *)malloc(sizeof(pthread_t)*num))==NULL)|| ((file=(struct copy_file *)malloc(sizeof(struct copy_file)*num))==NULL) ) { fprintf(stderr,"Out Of Memory!\n\a"); exit(1); } for(i=0,j=0;i d_name); if(stat(filename,&filestat)==-1) { fprintf(stderr,"Get File Information:%s\n\a",strerror(errno)); exit(1); } /* 我们忽略目录 */ if(!S_ISREG(filestat.st_mode))continue; if((file[j].infile=open(filename,O_RDONLY))<0) { fprintf(stderr,"Open %s Error:%s\n\a",filename,strerror(errno)); continue; } strcat(filename,".bak"); if((file[j].outfile=open(filename,O_WRONLY|O_CREAT,S_IRUSR|S_IWUSR))<0) { fprintf(stderr,"Creat %s Error:%s\n\a",filename,strerror(errno)); continue; } /* 创建线程,进行文件拷贝 */ if(pthread_create(&thread[j],NULL,copy,(void *)&file[j])!=0) fprintf(stderr,"Create Thread[%d] Error:%s\n\a",i,strerror(errno)); j++; } byte_copy=0; for(i=0;i 2.线程的同步和互斥
还记得我们在Linux进程通信一篇中叙述的POSIX无名信号量吗?由于Linux不支持进程之间的无名信号量.所以我们在那个地方没有提供实例.不过Linux提供了线程之间的无名信号量.我们可以使用POSIX无名信号量来处理线程之间的资源共享.下面以一个实际例子来说明如何进行线程同步.
/* 下面的程序如果正常运行的话,应该输出5行Language C For Linux */ /* sem.c */ #include#include #include #include #ifdef USE_SEM #include sem_t res; #endif void *thread_init(void *arg) { char *string="Language C For Linux\n"; int n,i; n=strlen(string); #ifdef USE_SEM sem_wait(&res); #endif for(i=0;i {
pthread_t thread[5]; int i,n; n=sizeof(thread)/sizeof(thread[0]); #ifdef USE_SEM /* 这里我们的第一个参数是0表示只在一个进程内部使用,第二个参数为1 表示我们只有一个信号量(一个标准输出) 请参考POSIX无名信号量 */ sem_init(&res,0,1); #endif for(i=0;i如果我们没有定义USE_SEM的来编译这个程序.运行以後的结果是一个没有按照我们希望的结果输出的.
$ gcc -O2 -Wall -o sem sem.c -lpthread $ ./sem这个程序的输出由于我们没有很好的控制线程之间的同步,几个线程会去争夺标准输出这个共享资源,而倒置我们的程序输出结果是杂乱无章的.
如果我们进行了线程的同步,也就事定义了USE_SEM宏.程序才会按照我们设想的方式运行 .
$ gcc -O2 -Wall -DUSE_SEM -o sem sem.c -lpthread -WALL $ ./sem进行线程同步的第二种办法是使用互斥锁(mutex)
#includeint pthread_mutex_init (pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); int pthread_mutex_destroy (pthread_mutex_t *mutex); int pthread_mutex_lock (pthread_mutex_t *mutex); int pthread_mutex_trylock (pthread_mutex_t *mutex); int pthread_mutex_unlock (pthread_mutex_t *mutex); 如果我们要使用互斥锁我们首先必须声明一个pthread_mutex_t的结构.然後要么调用 p thread_mutex_init对这个互斥锁进行初始化,要么我们在定义这个互斥锁的时候使用 P THREAD_MUTEX_INITILIZER进行初始化. 一旦我们创建好了互斥锁我们就可以进行对锁的锁住和解锁操作. 调用pthread_mutex_ lock会去锁住互斥锁,如果互斥锁被其它线程锁住了,线程会被祖塞. 直到这个互斥锁被解锁.调用pthread_mutex_unlock可以释放被锁住的互斥锁. 同时会唤醒那些正在等待这个互斥锁解锁的线程.如果我们不希望线程被阻塞. 我们可以调用pthread_mutex_trylo ck函数,如果互斥锁没有被其它线程锁住. 这个函数会去锁住互斥锁,否则这个函数立即返回.同时返回错误代码为EBUSY的错误.
#include#include #include #include #ifdef USE_SEM #include sem_t res; #elif USE_MUTEX #ifdef USE_STATIC pthread_mutex_t mutex_lock=PTHREAD_MUTEX_INITIALIZER; #else pthread_mutex_t mutex_lock; #endif #endif void *thread_init(void *arg) { char *string="Language C For Linux\n"; int n,i; n=strlen(string); #ifdef USE_SEM sem_wait(&res); #elif USE_MUTEX pthread_mutex_lock(&mutex_lock); #endif for(i=0;i 这个程序是上面的程序的改进.我们使用了互斥锁来进行线程的同步操作.在这个程序中我们使用了2中方法来进行互斥锁的初始化.一个是使用静态的初始化,一个是使用函数的初始话.可以分别使用下面的命令来进行编译.
使用函数初始化
$gcc -O2 -Wall -DUSE_MUTEX mutex mutex.c -lpthread $./mutex使用静态变量初始化
$gcc -O2 -Wall -DUSE_MUTEX -DUSE_STATIC mutext mutex.c -lpthread $./mutex3.线程的条件变量
假设线程必须等待某个包含全局变量集合的一个测试条件(比如有2个全局变量x,y.线程必须在x,y满足一定的条件的时候才执行).由于使用全局变量对于线程来说是临界区的东西,所以多全局变量的访问要进行互斥锁的操作.线程的条件变量为线程提供了一种等待包含这些全局变量的测试条件.不管什么时候,线程改变了这些全局变量的一个,线程就会条件变量上发出信号,通知阻塞的线程,条件改变了.这个信号会唤醒在等待条件的线程.当一个阻塞的线程收到信号以後,必须测试条件是否成立,因为信号只是表示全局变量改变了 ,并不意味着条件一定会成立.所以线程一定要测试条件是否成立.由于线程测试的时候要访问全局变量,所以线程首先要使用互斥锁.如果条件不成立,线程应该要释放互斥锁同时阻塞自己.而且互斥要在是否互斥锁之间释放,以便其它线程可以去访问全局变量.并可以改变全局变量的值,锁的释放和线程的阻塞必须是原子操作.这样其它的线程就不会在线程释放互斥锁和阻塞之前修改全局变量.
下面我们介绍一样线程条件变量的函数
#includeint pthread_cond_init (pthread_cond_t *cond, const pthread_condattr_t *arrt); int pthread_cond_destroy (pthread_cond_t *cond); int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex); int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime); int pthread_cond_signal (pthread_cond_t *cond); int pthread_cond_broadcast (pthread_cond_t *cond); pthread_cond_init 和pthread_mutex_init一样,可以初始化条件变量.当然我们也可以使用静态的初始化.pthread_cond_destroy用来删除信号量.pthread_cond_wait用来阻塞线程直到被pthread_cond_signal或者是pthread_cond_broadcast唤醒.
pthread_cond_timedwait也用来阻塞线程不过在时间abstime超时以後返回.
为了很好的说明互斥锁,我们使用一个实例来进行说明. 假设cond表示条件变量.mutex表示互斥锁.x==y表示我们要满足的测试条件.我们可以有下面的伪代码:
#includepthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond =PTHREAD_COND_INITIALIZER; static int x = 1; staitc int y = 2; /* 下面的这个代码用来检测条件x==y是否成立. */ 1) pthread_mutex_lock(&mutex); 2) while(x!=y) 3) pthread_cond_wait(&cond,&mutex); 4) .... /* 进行我们的操作 */ 5) pthread_mutex_unlock(&mutex); /* 下面的代码用来改变x,y的值,使它们去满足一定的条件 */ 6) pthread_mutex_lock(&mutex); 7) x++; 8) pthread_cond_signal(&cond); 9) pthread_mutex_unlock(&mutex); 我们来分析一下上面的伪代码.
我们考虑一个线程从1)开始执行,一个从6)开始执行.如果线程一首先开始执行了1)锁住了 mutex然後执行了2)进行条件测试.由于我们的x!=y成立,然後线程执行3)线程这个时候阻塞了.按照线程条件变量的要求,线程在阻塞之前隐式的(没有调用pthread_mutex_unlock) 来释放互斥锁.这个时候线程二开始执行6).由于线程一隐式的释放了互斥锁,所以线程二获得了互斥锁.可以执行7)接着执行8).这个时候线程二唤醒线程一,同时线程二继续执行. 线程一接收到了信号,由于线程一在pthread_cond_wait中隐式的释放了互斥锁,所以这个时候线程一又隐式的去锁住互斥锁不过这个时候互斥锁还被线程二拥有,所以线程一在隐式的锁住互斥锁处阻塞.直到线程二执行完9)以後,线程一才从3返回,由于线程一不知道条件是否成立,所以线程继续执行2)测试条件.条件成立,线程一开始执条件是否成立,所以线程继续执行2)测试条件.条件成立,线程一开始执行其它操作. 由于我们隐式的锁住了互斥锁,所以最後要释放互斥锁. 从上面的分析和从pthread_cond_wait的原型我们可以"猜测"一下pthread_cond_wait所执行的操作
/* 注意实际情况肯定不是这样的,我这样写只是我们更容易离解一些 */ ... /* 从pthread_cond_wait中隐式的释放互斥锁 */ 1) pthread_mutex_unlock(&mutex); ... /* 阻塞 */ /* 从pthread_cond_wait中隐式的锁住互斥锁 */ 2) pthread_mutex_lock(&mutex); ...