linux下多进程和多线程编程之二(同步)

来源:互联网 发布:windows live账号注册 编辑:程序博客网 时间:2024/04/30 11:08

多进程,多线程的同步

 多进程和多线程的同步,我们重点讲解下多线程下的同步,这个是编程中最常用的。

多线程同步,总结以下几种机制方式

第一:互斥锁

互斥锁的原理是利用互斥性,保证程序在某一刻只有一个线程执行一段关键部分的代码。

互斥变量的创建:有两种方式,第一个是pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIER。利用赋值的方式做静态赋值

                                      第二个是利用函数int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr_t *mutexattr);进行初始化一个互斥变量

然后使用函数int pthread_mutex_lock(pthread_mutex *mutex);顾名思义,这个函数就是对其进行上锁。然后开始执行关键部分的代码。

最后需要解锁使用int pthread_mutex_unlock(pthread_mutex *mutex); 这个函数就是对其进行解锁,然后其它线程才能获取到锁,进行上锁

等这把锁使用完毕,程序退出时,我们需要进行将锁释放,使用函数int pthread_mutex_destroy(pthread_mutex *mutex);

如下代码所示

#include<pthread.h>#include<stdio.h>#include<unistd.h>void* fun(void* i);pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;//用赋值的方法初始化一个互斥量,一把锁int count;//定义一个全局变量,让所有的线程可以共享这个全局变量int main(int argc,char* argv[]){        int i=0;        pthread_t t[10];        for(i=0;i<10;i++)        {                pthread_create(&t[i],NULL,&fun,NULL);        }        for(i=0;i<10;i++)        {                pthread_join(t[i],NULL);        }}void* fun(void* i){        if(pthread_mutex_lock(&mutex))//执行成功返回0        {                //失败的时候,需要做相应的处理        }        printf("count=%d\n",count++);//因为需要对这个全局变量进行操作,所以我们需要进行加锁,这部分可以称为是关键代码,也是线程需要同步的代码部分        if(!pthread_mutex_unlock(&mutex))//执行成功返回0        {                //如果解锁失败了.那么我们需要做相应的处理,因为如果解锁失败那么其它几个线程可能会出现死锁现象,程序阻塞        }}


以上程序运行结果是:

count=0
count=1
count=2
count=3
count=4
count=5
count=6
count=7
count=8
count=9
利用互斥量保证了线程的并发和同步,这里需要对线程加解锁函数调用做一次判断,以免程序出现死锁问题.

 

第二:条件变量

条件变量的产生是为了当多线程下,某个条件产生的时候,去执行一段代码。通常条件变量和互斥锁同时使用。

条件变量的创建和互斥量一样,都有两种方式,一种静态赋值,一种动态使用函数的方式。静态赋值的方式:pthread_cond_t cond=PTHREAD_COND_INITIALIZER,动态调用函数的方式:intpthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);尽管POSIX标准中为条件变量定义了属性,但在LinuxThreads中没有实现,因此cond_attr值通常为NULL,且被忽略.

条件变量的等待:有两种方式等待:第一是条件等待: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),计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待,其中abstime以与time()系统调用相同意义的绝对时间形式出现,0表示格林尼治时间1970年1月1日0时0分0秒。但是无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()(或pthread_cond_timedwait())的竞争条件(Race Condition)。

条件变量的激发:激发条件有两种形式,pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个;而pthread_cond_broadcast()则激活所有等待线程。

条件变量的注销:注销一个条件变量需要调用pthread_cond_destroy(),只有在没有线程在该条件变量上等待的时候才能注销这个条件变量,否则返回EBUSY。

条件变量的使用最经典的模式是生产者与消费者模式,下面就用代码描述这个模式。

#include<pthread.h>#include<stdio.h>#include<unistd.h>void* fun(void* i);void* fun1(void* i);pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;pthread_cond_t  ccon=PTHREAD_COND_INITIALIZER;pthread_cond_t  pcon=PTHREAD_COND_INITIALIZER;int     size=0;char store[5];void print()//打印{        int i=0;        for(i=0;i<size;i++)        printf("%c ",store[i]);        printf("\n");}int main(int argc,char* argv[]){        int i=0;        pthread_t t[10];        pthread_create(&t[0],NULL,&fun,NULL);//消费者        pthread_create(&t[1],NULL,&fun1,NULL);//生产者        pthread_create(&t[2],NULL,&fun1,NULL);//生产者        pthread_join(t[0],NULL);        pthread_join(t[1],NULL);        pthread_join(t[2],NULL);}void* fun(void* i)//消费者{        int count=0;        while(count<52)        {                if(pthread_mutex_lock(&mutex))//执行成功返回0                {                        //失败的时候,需要做相应的处理                }                if(size==0)//需要生产了,消费品已经消费完                {                        pthread_cond_wait(&ccon,&mutex);                }                printf("pop date:%c\n",store[size-1]);//消费                count++;                size--;//消费了,所以减去1                print();                pthread_cond_signal(&pcon);//通知生产着可以生产了                if(pthread_mutex_unlock(&mutex))//执行成功返回0                {                        //如果解锁失败了.那么我们需要做相应的处理,因为如果解锁失败那么其它几个线程可能会出现死锁现象,程序阻塞                }        }}void* fun1(void* i){        char    count='A';        while(count<='Z')        {                if(pthread_mutex_lock(&mutex))//执行成功返回0                {                        //失败的时候,需要做相应的处理                }                if(size==5)                {                        pthread_cond_wait(&pcon,&mutex);//等待消费者去消费                }                store[size++]=count;//生产了一个数据                printf("push data:%c\n",count);                count++;                print();                pthread_cond_signal(&ccon);//通知消费者消费,这个时候肯定是有可以消费的                pthread_mutex_unlock(&mutex);        }}


其中有两个生产者,一个消费者,这里需要注意的是,生产者和消费者尽量需要总生产数量和总消费数量一样。因为可能会出现,当消费者消费到M个退出 不在消费,也就不会再发激发信号,那么生产者继续生产,一直生产满了的时候,就会等消费者激发,但是这个时候消费者已经结束了,不再激发条件变量,导致了生产者进入了阻塞。如果不确定生产者和消费者的数量时候,我们可以用用pthread_cond_timedwait()函数去执行等待,设个时间,超过时间限制,就不再等待。

 总结条件变量:条件变量的运用,一般运用在多线程编程下面有一些代码需要满足某个条件成立的情况下才会执行的情况,条件变量也需要和互斥锁同时用。

 

第三:信号量

信号量不仅常作用于线程的同步,也作用于进程的同步,信号量的产生是为了满足一些资源在同一个时刻可以让N个线程或者进程同时访问,但是只能小于N个的进程或者线程同时访问,大于N的时候,就会被阻塞。信号量可以说是加强版的互斥锁,因为当信号量为1的时候,就是互斥锁的功能,也就某一个时刻只能一个线程使用。

 信号箱的函数使用一共有两套IPI函数,根据ipc的结构不同,一套是system ipc。一套是POSIX IPC。

POSIX IPC 的每个ipc都是有名称的,mq_open  sem_open  shm_open三个函数的第一个参数就是这个名称,这个名称不一定是在文件系统中存在的名称。要使用IPC对象,需要创建或者打开,这与文件操作类似,并且与文件类似,也有相对应的操作权限的。

System v ipc中有一个重要的类型是key_t,在msget、semget、shmget函数操作中都需要利用这个类型是参数。系统中对每个ipc对象都会有一个结构体来标识。

注:mq_open  shm_open和msget、shmget等函数将在多进程和多线程之通信中介绍。可参考那篇文章

信号灯分为两种:
一种是简单信号量(Posix ),另一种是用于进程间通讯的信号量集(System);
一简单信号量
属于POSIX标准的信号量;
从信号量的命名来看,信号量又可分为命名信号量和匿名(未命名)信号量;
从信号量的值来看,信号量可分为二进制信号量和计数信号量;
匿名信号量
匿名信号量是在内存中分配内存,进行初始化并由系统API进行管理的,它可以在多个线程之间进行资源同步,也可以在多个进程之间进行资源同步,这主要是看在初始化的时候给pshared传递的参数值,为0,则在线程之间同步,非0,则在进程之间同步;
命名信号量与匿名信号量不同,它一般只用于在进程之间进行资源同步,而且是使用文件全路径来对信号量进行命名的;命名信号量具有属主用户ID、组ID和保护模式等参数;命名信号量的名称是在文件系统的命名空间中定义的;
  匿名信号量的操作函数有:int sem_init(sem_t *sem, int pshared, unsigned int value);创建一个信号量,并根据value值来得到信号量的数量,pshared取0和非0,当其为0的时候,就是用于线程间的同步作用,当前为非0的时候,就是用于进程间的通信,但是由于现在linux目前没有实现进程间共享信号量,所以这个值只能是0。
int sem_wait(sem_t *sem);用于信号量的等待,也就是运行这个函数的时候,判断其sem值是否大于0,如果大于0,那么将其减1,然后执行后面的代码,如果等于0,那么就阻塞着,直到其值大于0的时候,争取到资源,与之相应的还有个sem_trywait()函数,这个是只会判断一次sem的值是否大于0,如果一旦判断不是大于0的,就返回一个错误,程序不会阻塞着。
int sem_pos(sem_t *sem);sem_post函数的作用是给信号量的值加1,它是一个原子操作---即同时对同一个信号量做加1操作的两个线程是不会冲突的(相当于锁住一样,保证多线程下不会有冲突发生).当然sem_wait也是一个原子操作,功能和sem_wait正好相反。
int sem_getvalue(sem_t *sem, int *sval);得到当前信号量的值,sval会将其当前信号量的值返回过来.反映当前还能支持多少的资源共享
sem_destroy是用来删除信号量。
#include<stdio.h>#include<pthread.h>#include<semaphore.h>#include<time.h>sem_t sem;//一个信号量void* run(void* par){        sem_wait(&sem);        printf("第%d个线程开始申请连接资源\n",(int)par+1);        sleep(5);        int x=0;        sem_getvalue(&sem,&x);        printf("第%d个线程开始释放资源\n",(int)par+1);        sem_post(&sem);}int main(int argc,char* argv[]){        sem_init(&sem,0,10);        pthread_t t[15];        int i=0;        for(i=0;i<15;i++)        {                pthread_create(&t[i],NULL,&run,(void*)i);        }        for(i=0;i<15;i++)        {                pthread_join(t[i],NULL);        }        sem_destroy(&sem);}

以上代码详细的描述了,信号量在多线程的时候对资源的申请,这里信号量有10个大小,保证资源在同一个时刻可以让十个线程对其操作.
 
  命名信号量的操作函数有:sem_open、sem_close、sem_wait(阻塞P操作(-1)、sem_trywait、sem_post(唤醒V操作(+1))、sem_getvalue、sem_unlink(删除系统中的信号灯);用法和匿名信号量一致,在此我也就不一一描述了。
因为命名信号量是对其文件的全路径进行申请的信号量,所以申请一个信号量的时候,就像打开一个文件一样如:sem_open(PATH,O_CREAT|O_EXCL,00666,2);
这里的PATH是文件的路径,O_CREAT|O_EXCL是创建命名信号量时候的方式,00666是创建时候的使用权限,2是信号量的数值。
ps:这里我们需要注意的是一般发行版本的linux内核不支持有名信号量的操作,需要在编译内核的时候,将其打开
还有一个信号量集:它属于System V信号量,是一个集合;常用于进程间的通讯;System V的IPC要求用于进程间通讯的信号量必须是一个集合;它是系统内核定义的一个数据结构;
信号量集的初始化操作由shmget函数完成;这点我们在多进程多线程通信上讲解,这里不做多的描述.读者可查看多进程多线程编程之三通信。
 
 

 

 

 

 

 


 

0 0
原创粉丝点击