线程同步

由于本文完全是本人的理解,该博文如果能对您有所帮助,不胜荣幸.如果有错误,或者有其他的建议,请私信我,我会及时的回复或修改.


首先,我们先来介绍一下,线程同步的一些方法:

          a.互斥量(互斥锁)    b.条件变量     c.信号量(pv原语操作)      d.读写锁     e.自旋锁      f.屏障 


下面我们依次来介绍这几种方式

        <1>互斥量

                互斥量本质上就是一把锁,由于它是互斥的,所以同一时间只能有一个线程可以获得相同的互斥锁,这样就可以 保证当多个线程并发访问同一资源的时候,给线程安排访问的次序

             互斥量的工作模式:
                    对互斥量加锁之后,任何试图再对该锁进行加锁的时候,线程就会堵塞.当一个线程解锁之后.如果有一个或  一个以上的线程堵塞,那么这些线程(全部)就会被唤醒,第一个获得锁的线程,重新对该互斥锁加锁,剩余的线程继续堵塞.

            信号量的操作函数:

                   int pthread_mutex_destroy(pthread_mutex_t *mutex);     //释放互斥锁所占的资源
                   int pthread_mutex_init(pthread_mutex_t *restrict mutex, //动态初始化互斥锁
                                                     const pthread_mutexattr_t *restrict attr);
                   pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//静态初始化互斥锁
                   int pthread_mutex_lock(pthread_mutex_t *mutex);        //加锁,成功返回0,失败返回错误编码

                   该函数是给互斥锁加锁的时候使用的,如果指定的互斥锁没有被锁上,那么就给这把互斥锁加上一把锁,如果指定的这个互斥锁是被锁上的,那么就会堵塞,等待指定的互斥锁可用

                   int pthread_mutex_trylock(pthread_mutex_t *mutex);    //尝试去给指定的互斥量加锁,成功返回0,失败返回错误编码

                   这个函数是一个不堵塞函数,如果指定的互斥锁没有被锁上,那么这个函数就锁上这把锁,返回0;如果这把锁是被锁上的,那么这个函数就不能给这把锁加锁,立即会返回错误编码(EBUSY);

                   int pthread_mutex_unlock(pthread_mutex_t *mutex);    //解锁,成功返回0,失败返回错误编码

先讨论一个问题,如果对一个没有加锁的互斥锁进行解锁的话,它会堵塞吗?

答案是:不会,但是在APUE书介绍,解一个没有加锁的互斥锁,将会产生不确定的效果,所以建议不要这样去做

请看下面的测试代码:

<span style="font-size:18px;">/*
	测试:在没有加锁的情况下能不能解锁
	结果:在没有加锁的状态可以解锁
*/

#include "fileheader.h"

pthread_mutex_t lock;

void* fun(void* arg)
{
	pthread_mutex_unlock(&lock);
	printf("can run\n");
}

int main(int argc, char const *argv[])
{
	pthread_t tid;
	pthread_mutex_init(&lock,NULL);
	pthread_create(&tid,NULL,fun,NULL);
	
	while(1)
	{

	}
	return 0;
}</span>


举一个简单的实例:

             

<span style="font-size:14px;">/*
	创建三个线程,第一个线程写文件,第二个线程读文件,第三个线程统计读取数据的单词数
在未加锁的状态下,可以进行解锁
*/
#include "fileheader.h"

pthread_mutex_t lock[3];  //0 : 表示读   1 : 表示写  2 : 表示统计
//规定先写,在读,在统计 

int fd;
void* myreadfile(void* arg)
{
	while(1)
	{		
		pthread_mutex_lock(&lock[0]);     //在此对write进行加锁
		printf("in myreadfile\n");
		lseek(fd,0,SEEK_SET);
		char buf[1024];
		read(fd,buf,1024);
		printf("read:%s\n",buf);
		pthread_mutex_unlock(&lock[2]);    
	}		
}

void* mywritefile(void* arg)
{
	while(1)
	{
		pthread_mutex_lock(&lock[1]);   //对写操作加锁
		printf("in mywritefile\n");
		printf("please input:");
		char buf[100];
		gets(buf);
		lseek(fd,0,SEEK_SET);
		write(fd,buf,strlen(buf));	
		pthread_mutex_unlock(&lock[0]);  
	}
}

void* mytotalchar(void* arg)
{
	while(1)
	{		
		pthread_mutex_lock(&lock[2]);   
		printf("in mytotalchar\n");
		int length=lseek(fd,SEEK_CUR,SEEK_END);
		printf("length:%d\n",length);
		lseek(fd,0,SEEK_SET);
		pthread_mutex_unlock(&lock[1]); //最后再对写操作解锁,执行写操作
	}	
}

int main(int argc, char const *argv[])
{
	int  i = 0;
	while(i<3)
	{
		pthread_mutex_init(&lock[i],NULL);	
		i++;
	}
	
	fd = open("data.dat",O_RDWR|O_CREAT|O_APPEND,0777);
	while(fd == -1)
	{
		fd = open("data.text",O_RDWR);
	}

	pthread_t tid[3];
	pthread_create(&tid[1],NULL,mywritefile,NULL);
	sleep(1);
	pthread_create(&tid[0],NULL,myreadfile,NULL);	
	sleep(1);
	pthread_create(&tid[2],NULL,mytotalchar,NULL);
	pthread_join(tid[0],NULL);
	pthread_join(tid[1],NULL);
	pthread_join(tid[2],NULL);
	return 0;
}
</span>

             上面函数出现的问题是:在第一次进行输入的时候,我们必须在1s之内输入,如果超过了1s,将会出现一些问题;   我将会在下面利用其它的锁去更正这个问题.

      

        <2>条件变量
           条件变量给多线程提供了一个会合的场所.当条件变量与互斥量一起使用的时候,允许线程以无竞争的方式等待
条件的发生.
          条件本身是由互斥量保护的.线程在改变条件之前必须首先锁住互斥量,其他线程在获得互斥量之前不会觉察到
这种改变,因为必须锁定互斥量之后,才能计算条件

            int pthread_cond_destroy(pthread_cond_t *cond);      //释放条件变量所占得资源
            int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);//初始化条件变量,attr一般为NULL,表示默认属性
            pthread_cond_t cond = PTHREAD_COND_INITIALIZER;        //静态方法初始化条件变量
            int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);  //它是一个堵塞型函数,等待条件的发生后,然后被唤醒执行.该函数有个特点,就是在睡眠之前,要先解锁,关闭了线程与条件发生之间的时间窗口,在被唤醒之后,要再把锁加上,这样就可以保证每个线程不会错过一个条件的变化,也与上面的说法相对应.
            int pthread_cond_broadcast(pthread_cond_t *cond);     //唤醒所有需要条件为cond的线程
            int pthread_cond_signal(pthread_cond_t *cond);           //唤醒一个需要条件为cond的线程
            同样的,我们这里也来探究一下在前面没有进行加锁的情况,也就是pthread_cond_wait无锁可解的时候,pthread_cond_wait会不会堵塞?
           

/*
	测试在没有互斥锁可以解的时候,pthread_cond_wait()的动作
	得出的结论:在pthread_cond_wait函数在没有锁可以解的时候,同样可以进入阻塞状态,
     在pthread_cond_signal(),去唤醒它的时候,一样可以唤醒
	在pthread_cond_wait()返回的时候,重新加锁,所以加锁的过程时pthread_cond_wait加的
*/
#include "fileheader.h"
pthread_cond_t  cond;
void handler(int signo)
{
	pthread_cond_signal(&cond);
}

int main(int argc, char const *argv[])
{
	signal(SIGINT,handler);
	pthread_mutex_t mutex;

	pthread_mutex_init(&mutex,NULL);
	pthread_cond_init(&cond,NULL);
	pthread_mutex_lock(&mutex);
	pthread_mutex_unlock(&mutex);
	pthread_cond_wait(&cond,&mutex);		
	printf("end\n");
	return 0;
}

答案也已经很明朗啦,我在我的电脑上(linux 2.6.23内核)测试的结果是不会堵塞的.

 下面看一个例子来进一步的了解这些函数的用法,以及为什么这么用?

       

#include "fileheader.h"

int data = 1;
pthread_cond_t cond;
pthread_mutex_t mutex;
//输出奇数
void fun(void* arg)
{
	while(data <= 5)
	{
		if(data%2!=0)
		{
			pthread_mutex_lock(&mutex);
			printf("fun:%d\n",data);
			data++;
		}
		else
		{
			if(pthread_mutex_trylock(&mutex)=``=0)
			{
				printf("fun1:no locked\n");
				pthread_mutex_unlock(&mutex);
			}
			else
			{
				printf("fun:locked\n");
			}
			pthread_cond_wait(&cond,&mutex);	//阻塞前:先解锁,在阻塞; 唤醒之后,先加锁,在执行
			if(pthread_mutex_trylock(&mutex)==0)
			{
				printf("fun1: second:no locked\n");
				pthread_mutex_unlock(&mutex);
			}
			else
			{
				printf("fun: second locked\n");
			}
			pthread_mutex_unlock(&mutex);   //这里是将pthread_cond_wait,加的锁解掉
			pthread_cond_signal(&cond);        
		}
	}
	pthread_cond_signal(&cond);   //这两句话的位置与while()内部的判断值,有关系,因为这关系到最后是哪个线程先可以执行完
	pthread_mutex_unlock(&mutex); //这里解锁的原因是,最后在pthread_cond_wait()加锁的时候,避免死锁的产生
}		
//输出偶数
void fun2(void* arg)
{
	while(data <= 5)
	{
		if(data%2 == 0)
		{
			pthread_mutex_lock(&mutex);
			printf("fun2:%d\n",data);
			data++;
		}
		else
		{
			pthread_cond_signal(&cond);
			if(pthread_mutex_trylock(&mutex)==0)
			{
				printf("fun2:no locked\n");
				pthread_mutex_unlock(&mutex);
			}
			else
			{
				printf("fun2:locked\n");
			}
			pthread_cond_wait(&cond,&mutex); //ddd
			pthread_mutex_unlock(&mutex);
		}
	}

}	

int main(int argc, char const *argv[])
{
    pthread_t  t1,t2;
    pthread_cond_init(&cond,NULL);
    pthread_mutex_init(&mutex,NULL);
    int ret=pthread_create(&t1,NULL,(void*)fun,NULL);
    while(ret!=0)
    {
         ret=pthread_create(&t1,NULL,(void*)fun,NULL);
    }

    ret=pthread_create(&t2,NULL,(void*)fun2,NULL);
    while(ret!=0)
    {
 	ret=pthread_create(&t2,NULL,(void*)fun2,NULL);
    }

    pthread_join(t1,NULL);
    pthread_join(t2,NULL);
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);	
    return 0;
}

        <3>信号量

         信号量,在这里又称为pv原语操作.也是线程同步的一种方式;

         先来看一下操作信号量的一些函数:

              int sem_init(sem_t *sem, int pshared, unsigned int value); //初始化信号量, 

              形参的意义:  sem   表示需要操作的信号量  pshared一般为0,表示在进程中的所有线程之间可以共享 

                                   value 表示信号量的初值

             

              int sem_wait(sem_t *sem);     

                        //对指定的信号量减一,当信号的值为0的时候,堵塞;也正是这一特点,它可以用于线程的同步操作中.

              int sem_getvalue(sem_t *sem, int *sval);//得到信号量的值
              int sem_post(sem_t *sem);     // 增加指定信号量的值,该函数不是堵塞函数 

         举个例子进行一下说明:

             

#include "fileheader.h"
int data=1;
sem_t sem1,sem2;
 void fun()
 {
    while(data<=10)
    {          	
    	sem_wait(&sem2);
    	printf("fun=%d\n",data);
    	data++;  
    	sem_post(&sem1); 
    }
 }

 void fun2()
 {
   while(data<=10)
   {   	
      sem_wait(&sem1);  
   	  printf("fun2=%d\n",data);
      data++;  
      sem_post(&sem2);	
   }
 }

 int main(int argc, char const *argv[])
 {
    pthread_t  t1,t2;
    sem_init(&sem1,0,0);
    sem_init(&sem2,0,1);
    int ret=pthread_create(&t1,NULL,(void*)fun,NULL);
    while(ret!=0)
    {
 	ret=pthread_create(&t1,NULL,(void*)fun,NULL);
    }

    ret=pthread_create(&t2,NULL,(void*)fun2,NULL);
    while(ret!=0)
    {
 	ret=pthread_create(&t2,NULL,(void*)fun2,NULL);
    }

    pthread_join(t1,NULL);
    pthread_join(t2,NULL);
    return 0;
 }
       这里对信号量的操作不做多的叙述.

        <4>读写锁

        读写锁的特点:
        a.当读写锁处于读模式锁住状态是,如果有另外的线程试图以写锁的模式去访问该资源的,那么读写锁通常会阻塞随后的读模式锁请求.这样可以避免读模式一直占用,而等待写模式锁请求却一直得不到请求读写锁适合对数据结构读的次数远大于写的情况.

       b.读读共享,读写互斥,写写互斥

         注意:不要因为读读是共享的,在释放的时候,就只释放一把锁,这样是不行的.如果是这样的话,非常容易造成死锁.\

      介绍一下操作读写锁的函数:

        int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);         //释放读写锁锁占用的资源
        int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,     //初始化读写锁
              const pthread_rwlockattr_t *restrict attr);      

       int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);    

         //尝试去获得指定的读写锁,如果该锁没有被占用,那么这个函数就将该读写锁锁住,返回0,否则返回出错编码

        int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);         //给指定的读写锁加写锁

        int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);         //给指定的读写锁加读锁

        int pthread_rwlock_unlock(pthread_rwlock_t* rwlock);        //不管是读锁还是写锁,都可以使用该函数进行解锁

         在讨论互斥量的时候我说第二个程序有点问题,这里我使用读写锁和互斥量一起使用,去解决这个问题

       修改过后的例子:

/*
	创建三个线程,第一个线程写文件,第二个线程读文件,第三个线程统计读取数据的单词数
*/

#include "fileheader.h"

int fd;
pthread_rwlock_t rwlock;
pthread_mutex_t mutexlock[2];
pthread_mutex_t lock[2];

void* myreadfile(void* arg)
{
	while(1)
	{
		pthread_mutex_lock(&lock[0]);   //read
		pthread_mutex_lock(&mutexlock[0]);
		//printf("locked read\n");
		pthread_rwlock_rdlock(&rwlock);		
		//printf("in myreadfile\n");
		lseek(fd,0,SEEK_SET);
		char buf[1024];
		read(fd,buf,1024);
		printf("read:%s\n",buf);
		pthread_rwlock_unlock(&rwlock);	
		pthread_mutex_unlock(&mutexlock[1]);
	}
} 

void* mywritefile(void* arg)
{
	pthread_mutex_lock(&mutexlock[0]);
	pthread_mutex_lock(&mutexlock[1]);
	while(1)
	{
		pthread_mutex_lock(&lock[1]);
		pthread_rwlock_wrlock(&rwlock);	
		// printf("in mywritefile\n");
		printf("please input:");
		char buf[100];
		gets(buf);
		lseek(fd,0,SEEK_SET);
		write(fd,buf,strlen(buf));	
			
		// if(pthread_mutex_trylock(&mutexlock[0]) == 0)   //这一段语句,是用来避免解锁没有加锁的互斥量的情况发生
		// {
		// 	printf("no locked\n");
		// 	pthread_mutex_unlock(&mutexlock[0]);
		// }
		// else
		// {
			//printf("locked\n");
			pthread_mutex_unlock(&mutexlock[0]);	
		//}
		pthread_rwlock_unlock(&rwlock);	
		pthread_mutex_unlock(&lock[0]);		
	}
}

void* totalfile(void* arg)
{
	while(1)
	{
		pthread_mutex_lock(&mutexlock[1]);
		//printf("locked total\n");
		pthread_rwlock_rdlock(&rwlock);			
		//printf("in mytotalchar\n");
		int length=lseek(fd,SEEK_CUR,SEEK_END);
		printf("length:%d\n",length-1);
		lseek(fd,0,SEEK_SET);
		pthread_rwlock_unlock(&rwlock);	
		pthread_mutex_unlock(&lock[1]);	
	}	
}

int main(int argc, char const *argv[])
{
	system("rm -f data.dat");
	pthread_t tid[3];
	fd = open("data.dat",O_RDWR|O_CREAT|O_APPEND,0777);
	while(fd == -1)
	{
		fd = open("data.text",O_RDWR);
	}
	pthread_rwlock_init(&rwlock,NULL);
	pthread_mutex_init(&mutexlock[0],NULL);
	pthread_mutex_init(&mutexlock[1],NULL);
	pthread_mutex_init(&lock[0],NULL);
	pthread_mutex_init(&lock[1],NULL);
	pthread_create(&tid[0],NULL,mywritefile,NULL);
	usleep(10000);
	pthread_create(&tid[1],NULL,myreadfile,NULL);
	//sleep(1);
	pthread_create(&tid[2],NULL,totalfile,NULL);
	int i = 0;
	for(i=0;i<3;i++)
	{
		pthread_join(tid[i],NULL);   //pthread_join()只能等待非分离线程
	}
	for(i=0;i<2;i++)
	{
		pthread_mutex_destroy(&mutexlock[i]);
		pthread_mutex_destroy(&lock[i]);
	}
	pthread_rwlock_destroy(&rwlock);
	return 0;
}

       

        <5>自旋锁

          自旋锁与互斥量类似,但是它和互斥量不同的地方在于,自旋锁是使线程在获得锁之前一直处于忙等状态(一直占用CPU),
而互斥量却是使线程进行休眠从而阻塞.
        自旋锁一般用于:持有时间比较短,而且线程不希望在重新调用方面花成本.
        在非抢占式内核中,自旋锁的用处比较大,除了提供类似于互斥量的阻塞机制之外,还会阻塞中断.这样中断就不会使系统进入死锁状态.因为中断它需要获得自旋锁,如果这把锁是没有被释放的,那么它一定会忙等待,因为中断处理程序是不能休眠的,所以只能用自旋锁.
    在用户层:运行在分时调度类中的线程有两种情况可能被抢占,
    1.自己运行的时间片到期了
    2.有更高级别的线程就绪进入可运行状态
    那么这个时候有一个线程拥有一把自旋锁,如果该线程发生了以上两种情况的一种的话,那么等待这把锁的其他线程等待的时间就会相对预期来说比较长
    int pthread_spin_lock(pthread_spinlock_t *lock);          //加锁
    int pthread_spin_trylock(pthread_spinlock_t *lock);      //试图加锁,与前面几个试图加锁的函数类似,不做赘述
    int pthread_spin_destroy(pthread_spinlock_t *lock);    //释放自旋锁所占用的资源
    int pthread_spin_init(pthread_spinlock_t *lock, int pshared);   //初始化自旋锁
        pshared选项值(表示该锁的共享属性):
                PTHREAD_PROCESS_PRIVATE   //表示只能被初始化该锁的进程内部的线程使用    
                PTHREAD_PROCESS_SHARED      //表示可以被其他的进程中的线程进行访问.    
    int pthread_spin_unlock(pthread_spinlock_t *lock);       //解除指定的自旋锁
    注意事项:
    在使用自旋锁的时候,注意尽量在持有自旋锁的线程中使用阻塞函数,因为这样会使其他线程等待的时间更久;
    在使用自旋锁的时候,尽量不要递归,因为递归的话,非常容易造成死锁

     举个例子:

    

#include "fileheader.h"
pthread_spinlock_t spinlock;
pthread_mutex_t mutexlock;

int data = 0;

void* fun1(void* arg)
{
	while(data <= 10)
	{
		//pthread_spin_lock(&spinlock);
		pthread_mutex_lock(&mutexlock);
		printf("fun1->data:%d\n",data);
		data++;		
		//pthread_spin_unlock(&spinlock);
		pthread_mutex_unlock(&mutexlock);
	}	
}

void* fun2(void* arg)
{
	while(data <= 10)
	{
		//pthread_spin_lock(&spinlock);
		pthread_mutex_lock(&mutexlock);
		printf("fun1->data:%d\n",data);
		data++;		
		//pthread_spin_unlock(&spinlock);
		pthread_mutex_unlock(&mutexlock);
	}
}

int main(int argc, char const *argv[])
{
	pthread_t tid[2];
	//pthread_spin_init(&spinlock,PTHREAD_PROCESS_PRIVATE);
	pthread_mutex_init(&mutexlock,NULL);
	pthread_create(&tid[0],NULL,fun1,NULL);
	pthread_create(&tid[1],NULL,fun2,NULL);
	pthread_join(tid[0],NULL);
	pthread_join(tid[1],NULL);
	return 0;
}


    详细自旋锁的信息,可以看一下我上一个关于自旋锁的讨论http://blog.csdn.net/cdcdsfs/article/details/50970776;

        <6>屏障

         该机制在有的低版本的内核中是不支持的.在2.6.23内核中可以使用该机制

        屏障:使用户协调多个线程并行工作的同步机制.
        屏障允许每个线程等待,直到所有的合作线程都到了一个点之后,然后再从该点继续执行.pthread_join就是一种屏障,允许一个线程等待,一个线程退出
        屏障对象的概念更广,他们允许任意数量的线程等待,直到所有的线程完成了处理工作,而线程不需要退出.所有线程
到达屏障之后可以继续工作
     int pthread_barrier_wait(pthread_barrier_t *barrier);
             //这个函数主要是用来表明,线程已完成工作,准备等所有其他线程赶上来;
        调用pthread_barrier_wait的线程在屏障技术未满足条件的时候,就会进入休眠状态.如果该线程是最后一个pthread_barrier_wait函数的线程,就满足了屏障技术,所有的线程都回被唤醒;
    对于最后一个调用(即执行完的线程数达到要求)pthread_barrier_wait函数的线程来说,pthread_barrier_wait的返回值是              PTHREAD_BARRIER_THREAD,其他线程返回的是0;这就可以使得一个线程可以作为主线程,它可以工作在其他所有线程已完成的工作结果上.这里当进程执行了相应数量的pthread_barrier_wait之后,然后就才会会返回;
一旦达到了屏障计数的数值的时候,屏障就会重新可用(清零);

     int pthread_barrier_destroy(pthread_barrier_t *barrier);         //释放屏障所占的资源
     int pthread_barrier_init(pthread_barrier_t *restrict barrier,      //初始化屏障
                        const pthread_barrierattr_t *restrict attr,
                        unsigned count);

    初始化屏障的时候,可以使用count参数指定,在pthread_barrier_wait()允许所有线程继续执行之前,必须到达屏障的线程数目. 

     举个例子说明:

     

#include "fileheader.h"

pthread_barrier_t b;
void* fun1(void* arg)
{
	printf("in fun1\n");
	if(pthread_barrier_wait(&b)==0)
	{
		printf(" == 0\n");
	}
	printf("leave fun1\n");
	return (void*)0;
}

int main(int argc, char const *argv[])
{
	printf("begin\n");
	pthread_t tid[3];
	int  i = 0;
	pthread_barrier_init(&b,NULL,4);
	for(i = 0;i < 3; i++)
	{
		pthread_create(&tid[i],NULL,fun1,NULL);
	}
	if(pthread_barrier_wait(&b)==PTHREAD_BARRIER_SERIAL_THREAD)
	{
		printf("==PTHREAD_BARRIER_SERIAL_THREAD\n");
	}
	printf("end\n");
	return 0;
}




你可能感兴趣的:(线程同步)