一、线程理论基础
1. 多线程
线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者。传统的Unix也支持线程的概念,但是在一个进程(process)中只允许有一个线程,这样多线程就意味着多进程。现在,多线程技术已经被许多操作系统所支持,包括Windows/NT、Linux。
2. 为什么有了进程,还要引入线程呢?使用多线程到底有哪些好处?
使用多线程的理由之一是:
和进程相比,它是一种非常“节俭”的多任务操作方式。在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。运行于一个进程中的多个线程,它们之间使用相同的地址空间,而且线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,一个进程的开销大约是一个线程开销的30倍左右。
使用多线程的理由之二是:
线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过进程间通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。除了以上所说的优点外,多线程程序作为一种多任务、并发的工作方式,有如下优点:
1)使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。2)改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。3. Linux系统下的多线程
Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a。
二、 多线程程序设计
1. 创建线程
#include <pthread.h> int pthread_create(pthread_t * tidp,const pthread_attr_t *attr, void *(*start_rtn)(void),void *arg) tidp:线程id attr:线程属性(通常为空) start_rtn:线程要执行的函数,函数返回值为空指针型 arg:start_rtn的参数,线程函数的参数,必须为指针型2. 编译
因为pthread的库不是linux系统的库,所以在进行编译的时候要加上-lpthread
# gcc filename -lpthread3. 实例分析
thread_int.c代码如下:
#include#include #include void *create(void *arg) // pthread_create()函数最后一个参数
{
int *num; num=(int *)arg; printf("creat parameter is %d\n",*num); return (void *)0;}int main(int argc, char *argv[]){ pthread_t tidp; int error; int test=4; int *attr=&test; error=pthread_create(&tidp,NULL,create,(void *)attr); // 参数类型强制类型转换 if(error) { printf("pthread_creat is created in not created ...\n"); return -1; } sleep(1); printf("pthread_create is created ...\n"); return 0;
}
thread_share.c代码如下:
#include#include int a=1;void *create(void *arg){ printf("new pthread ... \n"); printf("a=%d \n",a); return (void *)0;}int main(int argc,char *argv[]){ pthread_t tidp; int error; int a=5; error=pthread_create(&tidp,NULL,create,NULL); if(error!=0) { printf("new thread is not create ... \n"); return -1; } sleep(1); printf("new thread is created ... \n"); return 0;}
全局变量在线程函数中也是有效的。
thread_struct.c代码如下:
#include#include #include #include struct menber{ int a; char *s;};/*线程执行函数*/void *create(void *arg){ struct menber *temp; temp=(struct menber *)arg; printf("menber->a = %d \n",temp->a); printf("menber->s = %s \n",temp->s); return (void *)0;}int main(int argc,char *argv[]){ pthread_t tidp; int error; struct menber *b; /*为结构体指针b分配内存并赋值*/ b=(struct menber *)malloc( sizeof(struct menber) ); b->a = 4; b->s = "zieckey"; /*创建线程并运行线程执行函数*/ error = pthread_create(&tidp, NULL, create, (void *)b); if( error ) { printf("phread is not created...\n"); return -1; } sleep(1); //进程睡眠一秒使线程执行完后进程才会结束 printf("pthread is created...\n"); return 0;}
4.终止线程
如果进程中任何一个线程中调用exit或_exit,那么整个进程都会终止。线程的正常退出方式有:
(1) 线程从启动例程中返回(2) 线程可以被另一个进程终止(3) 线程自己调用pthread_exit函数#include <pthread.h>
void pthread_exit(void * rval_ptr) 功能:终止调用线程 Rval_ptr:线程退出返回值的指针,这个值pthread_join()函数可以接受。5. 线程等待
#include <pthread.h>
int pthread_join(pthread_t tid,void **rval_ptr) 功能:阻塞调用线程,直到指定的线程终止。 Tid :等待退出的线程id Rval_ptr:线程退出的返回值的指针,函数pthread_exit()函数或return返回的值。thread_join.c代码如下:
#include#include #include void *thread(void *str){ int i; for (i = 0; i < 3; ++i) { sleep(2); printf( "This in the thread : %d\n" , i ); } return NULL;}int main(){ pthread_t pth; int i; /*创建线程并执行线程执行函数*/ int ret = pthread_create(&pth, NULL, thread, NULL); printf("The main process will be to run,but will be blocked soon\n"); /*阻塞等待线程退出*/ pthread_join(pth, NULL); printf("thread was exit\n"); for (i = 0; i < 3; ++i) { sleep(1); printf( "This in the main : %d\n" , i ); } return 0;}
6. 线程标识
#include <pthread.h> pthread_t pthread_self(void) 功能:获取调用线程的 thread identifier7. 清除
线程终止有两种情况:正常终止和非正常终止。线程主动调用pthread_exit或者从线程函数中return都将使线程正常退出,这是可预见的退出方式;非正常终止是线程在其他线程的干预下,或者由于自身运行出错(比如访问非法地址)而退出,这种退出方式是不可预见的。不论是可预见的线程终止还是异常终止,都会存在资源释放的问题,如何保证线程终止时能顺利的释放掉自己所占用的资源,是一个必须考虑解决的问题。
从pthread_cleanup_push的调用点到pthread_cleanup_pop之间的程序段中的终止动作(包括调用pthread_exit()和异常终止,不包括return)都将执行pthread_cleanup_push()所指定的清理函数。
#include <pthread.h>
void pthread_cleanup_push(void (*rtn)(void *),void *arg) 功能:将清除函数压入清除栈 Rtn:清除函数 Arg:清除函数的参数#include <pthread.h>
void pthread_cleanup_pop(int execute) 功能:将清除函数弹出清除栈 参数:Execute执行到pthread_cleanup_pop()时是否在弹出清理函数的同时执行该函数,非0:执行; 0:不执行thread_clean.c代码如下:
#include#include #include /*线程清理函数*/void *clean(void *arg){ printf("cleanup :%s\n",(char *)arg); return (void *)0;}/*线程1的执行函数*/void *thr_fn1(void *arg){ printf("thread 1 start \n"); /*将线程清理函数压入清除栈两次*/ pthread_cleanup_push( (void*)clean,"thread 1 first handler"); pthread_cleanup_push( (void*)clean,"thread 1 second hadler"); printf("thread 1 push complete \n"); if(arg) { return((void *)1); //线程运行到这里会结束,后面的代码不会被运行。由于是用return退出,所以不会执行线程清理函数。 } pthread_cleanup_pop(0); pthread_cleanup_pop(0); return (void *)1;}/*线程2的执行函数*/void *thr_fn2(void *arg){ printf("thread 2 start \n"); /*将线程清理函数压入清除栈两次*/ pthread_cleanup_push( (void*)clean,"thread 2 first handler"); pthread_cleanup_push( (void*)clean,"thread 2 second handler"); printf("thread 2 push complete \n"); if(arg) { pthread_exit((void *)2);//线程运行到这里会结束,后面的代码不会被运行。由于是用pthread_exit退出,所以会执行线程清理函数。执行的顺序是先压进栈的后执行,即后进先出。 } pthread_cleanup_pop(0); pthread_cleanup_pop(0); pthread_exit((void *)2);}int main(void){ int err; pthread_t tid1,tid2; void *tret; /*创建线程1并执行线程执行函数*/ err=pthread_create(&tid1,NULL,thr_fn1,(void *)1); if(err!=0) { printf("error .... \n"); return -1; } /*创建线程2并执行线程执行函数*/ err=pthread_create(&tid2,NULL,thr_fn2,(void *)1); if(err!=0) { printf("error .... \n"); return -1; } /*阻塞等待线程1退出,并获取线程1的返回值*/ err=pthread_join(tid1,&tret); if(err!=0) { printf("error .... \n"); return -1; } printf("thread 1 exit code %d \n",(int)tret); /*阻塞等待线程2退出,并获取线程2的返回值*/ err=pthread_join(tid2,&tret); if(err!=0) { printf("error .... "); return -1; } printf("thread 2 exit code %d \n",(int)tret); return 1;}
三、线程同步
进行多线程编程,因为无法知道哪个线程会在哪个时候对共享资源进行操作,因此让如何保护共享资源变得复杂,通过下面这些技术的使用,可以解决线程之间对资源的竞争:
1)互斥量Mutex2)信号灯Semaphore3)条件变量Conditions
为什么需要互斥量:
Item * p =queue_list;Queue_list=queue_list->next;process_job(p);free(p);当线程1处理完Item *p=queue_list后,系统停止线程1的运行,改而运行线程2。线程2照样取出头节点,然后进行处理,最后释放了该节点。过了段时间,线程1重新得到运行。而这个时候,p所指向的节点已经被线程2释放掉,而线程1对此毫无知晓。他会接着运行process_job(p)。而这将导致无法预料的后果!对于这种情况,系统给我们提供了互斥量。线程在取出头节点前必须要等待互斥量,如果此时有其他线程已经获得该互斥量,那么该线程将会阻塞在这里。只有等到其他线程释放掉该互斥量后,该线程才有可能得到该互斥量。互斥量从本质上说就是一把锁, 提供对共享资源的保护访问。
在Linux中, 互斥量使用类型pthread_mutex_t表示。在使用前, 要对它进行初始化:
1)对于静态分配的互斥量, 可以把它设置为默认的mutex对象PTHREAD_MUTEX_INITIALIZER2)对于动态分配的互斥量, 在申请内存(malloc)之后, 通过pthread_mutex_init进行初始化, 并且在释放内存(free)前需要调用pthread_mutex_destroy。创建
#include <pthread.h> 1)int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr)2)int pthread_mutex_destroy(pthread_mutex_t *mutex)加锁
对共享资源的访问, 要使用互斥量进行加锁, 如果互斥量已经上了锁, 调用线程会阻塞, 直到互斥量被解锁。1)int pthread_mutex_lock(pthread_mutex_t *mutex)2)int pthread_mutex_trylock(pthread_mutex_t *mutex)返回值: 成功则返回0, 出错则返回错误编号。trylock是非阻塞调用模式, 如果互斥量没被锁住, trylock函数将对互斥量加锁, 并获得对共享资源的访问权限; 如果互斥量被锁住了, trylock函数将不会阻塞等待而直接返回EBUSY, 表示共享资源处于忙状态。解锁
在操作完成后,必须给互斥量解锁,也就是前面所说的释放。这样其他等待该锁的线程才有机会获得该锁,否则其他线程将会永远阻塞。
int pthread_mutex_unlock(pthread_mutex_t *mutex)互斥量PK信号量
1)Mutex是一把钥匙,一个人拿了就可进入一个房间,出来的时候把钥匙交给队列的第一个。2)Semaphore是一件可以容纳N人的房间,如果人不满就可以进去,如果人满了,就要等待有人出来。对于N=1的情况,称为binary semaphore。3)Binary semaphore与Mutex的差异:1. mutex要由获得锁的线程来释放(谁获得,谁释放)。而semaphore可以由其它线程释放2. 初始状态可能不一样:mutex的初始值是1 ,而semaphore的初始值可能是0(或者为1)。