线程同步----信号量、互斥量、读写锁、条件变量

        当多个控制线程共享相同的内存时,需要确保每个线程看到一致的数据视图。如果每个线程使用的变量都是其他线程不会读取或修改的,那么就不存在一致性问题。 同样地,如果变量是只读的,多个线程同时读取该变量也不会有一致性问题。但是当某个线程可以修改变量,而其他线程也可以读取或者修改这个变量的时候,就需要对这些线程进行同步,以确保它们在访问变量的存储内容时不会访问到无效的数值。
       线程同步的方法:信号量、互斥量、读写锁、条件变量等。
        1、信号量
        信号量是一个特殊类型的变量,它可以被增加或减少,对其关键访问是保证原子操作,这意味着如果一个程序中有两个(或更多)的线程试图改变一个信号量的值,系统将保证所有的操作都将依次进行,但如果是普通变量,来自同一程序中的不同线程的冲突操作所导致的结果将是不确定的。常用的是二进制信号量,它只有0和1两种取值,还有一种更通用的信号量是计数信号量, 它可以有更大的取值范围。信号量一般常用来保护一段代码, 使其每次只能被一个执行线程运行,要完成这个工作,就要使用二进制信号量。
        信号量用sem_t数据类型来表示,它本质上是一个长整型的数,在使用信号量之前,必须首先对它进行初始化,可以通过调用sem_init函数进行初始化,在释放内存前需要调用sem_destroy函数销毁。

#include <semaphore.h>
int sem_init(sem_t *sem,int pshared,usigned int value);
int sem_destroy(sem_t *sem);
返回值:若成功则返回0,否则返回错误编号。

        sem_init这个函数的作用是对由sem指定的信号量进行初始化,设置好它的共享选项,并指定一个整数类型的初始值。sem一般定义在线程共享的全局数据区,pshared参数控制着信号量的类型。如果 pshared的值是0,就表示它是当前进程的局部信号量,否则,其它进程就能够共享这个信号量。Linux线程目前不支持进程间共享信号量,pshared传递一个非零将会使函数返回ENOSYS错误。
        在linux的线程中,其实是没有任何资源关联到信号量对象需要释放的,因此在linux中,销毁信号量对象的作用仅仅是测试是否有线程因为该信号量在等待,如果函数返回0说明没有,正常注销信号量,如果返回EBUSY,说明还有线程正在等待该信号量的信号。
        下面这三个函数控制信号量的值,如下所示:

#include <semaphore.h>
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem);

返回值:若成功则返回0,否则返回错误编号。

        这三个函数都以一个指针为参数,该指针指向的对象是由sem_init调用初始化的信号量。       
        sem_wait函数是一个p操作,它的作用是从信号量的值减去一个“1”,当信号量的值大于0时,调用sem_wait()函数线程将会继续执行,当信号量的值等于0时,调用sem_wait()函数的线程就会等待,直到有其他线程增加了该信号量的值使其不再是0为止。如果两个线程同时在sem_wait调用上等待同个信号量变为非零,那么当该信号量被第三个线程增加时,只有其中一个等待线程将开始对信号量减1,然后继续执行,另外一个线程还将继续等待。
        sem_trywait函数是sem_wait函数的非阻塞搭档,即调用sem_trywait函数不会阻塞,当信号量的值大于0时,调用sem_wait()函数线程将会继续执行,返回值为0,当信号量的值等于0时,调用sem_wait()函数会失败,返回EAGAIN。
        sem_post函数是一个V操作,它的作用是给信号量的值加上一个“1”。
        例:主线程负责接收用户输入的字符串,函数线程将用户输入的字符串转变为大写并输出

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<pthread.h>
#include<semaphore.h>

char buff[128]={0};
sem_t sem;

//函数线程
void* fun(void *arg)
{
	while(1)
	{
		sem_wait(&sem);//p操作
		if(strncmp(buff,"end",3)==0)
		{
			break;
		}

		int i=0;
		for(;buff[i]!=0;++i)
		{
			if(buff[i]>='A' && buff[i]<='Z')
			{
				printf("%c",buff[i]);
				
			}
			else
			{
				printf("%c",buff[i]-'a'+'A');
			}
		}
		printf("\n");
		sem_post(&sem);//v操作
		sleep(1);
	}
}

//函数线程
void Destroy()
{
	printf("Destroy was called\n");
	sem_destroy(&sem);

}

//主线程
int main()
{
	atexit(Destroy);
	sem_init(&sem,5,1);//初始化信号量
	sem_wait(&sem);//p操作
	pthread_t id;
	int res=pthread_create(&id,NULL,fun,NULL);
	assert(res==0);

	while(1)
	{
		printf("pleasse input:");
		fgets(buff,128,stdin);
		buff[strlen(buff)-1]=0;
		sem_post(&sem);//v操作
		if(strncmp(buff,"end",3)==0)
		{
			break;
		}
		sleep(1);
		sem_wait(&sem);//p操作
	}
}

运行结果如下:

         2、互斥量
        互斥量从本质上说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥锁。如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程都会变成可运行状态,第一个变为运行状态的线程可以对互斥量加锁,其他线程将会看到互斥锁依然被锁住,只能回去再次等待它重新变为可用。在这种方式下,每次只有一个线程可以向前执行。
        在设计时需要规定所有的线程必须遵守相同的数据访问规则,只有这样,互斥机制才能正常工作。操作系统并不会做数据访问的串行化。如果允许其中的某个线程在没有得到锁的情况下也可以访问共享资源,那么即使其他的线程在使用共享资源前都获取了锁,也还是会出现数据不一致的问题。
        互斥变量用pthread_ mutex_ t数据类型来表示,在使用互斥变量之前,必须首先对它进行初始化,可以把它置为常PTHREA D_MUTEX_ INITIALIZER (只对静态分配的互斥量),也可以通过调用pthread_ mutex_ init函数进行初始化,如果动态地分配互斥量(例如通过调用malloc函数),需要在释放内存前需要调用pthread_mutex_destroy。

#include <pthread.h> 
int pthread_mutex_init (pthread_mutex_ t *restrict mutex,const pthread_mutexattr_t *restrict attr) ;
int pthread_mutex_destroy(pthread_mutex_t *mutex) ;
返回值:若成功则返回0,否则返回错误编号。

        将attr设置为NULL表示使用默认的属性初始化互斥量。
        对互斥量进行加锁,需要调用pthread_ mutex_ lock,如果互斥量已经上锁,调用线程将阻塞直到互斥量被解锁。对互斥量解锁,需要调用pthread_ mutex_ unlock。

#include <pthread. h>
int pthread_mutex_lock(pthread_mutex_t *mutex) ;
int pthread mutex_trylock (pthread_mutex_t *mutex);
int pthread_mutex_unlock (pthread_mutex_t *mulex);
返回值:若成功则返回0,否则返回错误编号。

        如果线程不希望被阻塞,可以使用pthread_mutex_trylock尝试对互斥量进行加锁,如果调用pthread_mutex_trylock时互斥量处于未锁住状态,那么pthread_mutex_trylock将锁住互斥量,不会出现阻塞并返回0,如果调用pthread_mutex_trylock时互斥量处于锁住状态,那么pthread_mutex_trylock将锁住互斥量就会失败,不能锁住互斥量,返回EBUSY。
  例:主线程负责接收用户输入的字符串,函数线程将用户输入的字符串转变为大写并输出

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<pthread.h>
#include<semaphore.h>

char buff[128]={0};
pthread_mutex_t mutex;
pthread_mutex_t mutex2;

//函数线程
void* fun(void *arg)
{
	while(1)
	{	
		pthread_mutex_lock(&mutex);//加锁
	
		if(strncmp(buff,"end",3)==0)
		{
			break;
		}

		int i=0;
		for(;buff[i]!=0;++i)
		{
			if(buff[i]>='A' && buff[i]<='Z')
			{
				printf("%c",buff[i]);
				
			}
			else
			{
				printf("%c",buff[i]-'a'+'A');
			}
		}
		printf("\n");
		pthread_mutex_unlock(&mutex2);//解锁
	}
}

//函数线程
void Destroy()
{
	printf("Destroy was called\n");
	
	pthread_mutex_destroy(&mutex);
}

//主线程
int main()
{
	atexit(Destroy);
	pthread_mutex_init(&mutex,NULL);//初始化锁
	pthread_mutex_init(&mutex2,NULL);//初始化锁
	pthread_mutex_lock(&mutex);//加锁
	
	pthread_t id;
	int res=pthread_create(&id,NULL,fun,NULL);
	assert(res==0);

	while(1)
	{
		pthread_mutex_lock(&mutex2);//加锁
		printf("pleasse input:");
		fgets(buff,128,stdin);
		buff[strlen(buff)-1]=0;
		pthread_mutex_unlock(&mutex);

		if(strncmp(buff,"end",3)==0)
		{
			break;
		}
	}
}


运行结果如下:

        3、读写锁
        
读写锁与互斥量类似,不过读写锁允许更高的并行性。互斥量要么是锁住状态要么是不加锁状态,而且一次只有一个线程可以对其加锁。读写锁可以有三种状:读模式下加锁状态、写模式下加锁状态、不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
        当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是如果线程以写模式对此锁进行加锁,它必须阻塞直到所有的线程释放读锁。虽然读写锁的实现各不相同,但当读写锁处于读模式锁住状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。
        读写锁非常适合于对数据结构读的次数远大于写的情况。当读写锁在写模式下时,它所保护的数据结构就可以被安全地修改,因为当前只有一个线程可以在写模式下拥有这个锁。当读写锁在读模式下时,只要线程获取了读模式下的读写锁,该锁所保护的数据结构可以被多个获得读模式锁的线程读取。
        读写锁也叫做共享-独占锁,当读写锁以读模式锁住时,它是以共享模式锁住的:当它以写模式锁住时,它是以独占模式锁住的。
        
与互斥量一样,读写锁在使用之前必须初始化,在释放它们底层的内存前必须销毁。

#include <pthread. h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_ t *restrict attr) ;
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock) ;
返回值:若成功则返回0,否则返回错误编号

        将attr设置为NULL表示使用默认的属性初始化读写锁。     
        在释放读写锁占用的内存之前,需要调用pthread_ rwlock_ destroy做清理工作。pthread_rwlock_init为读写锁分配了资源,pthread_rwlock_destroy将释放这些资源。如果在调用pthread_rwlock_destroy之前就释放了读写锁占用的内存空间,那么分配给这个锁的资源就丟失了。
        要在读模式下锁定读写锁,需要调用pthread_rwlock_rdlock,要在写模式下锁定读写锁,需要调用pthread_rwlock_wrlock,不管以何种方式锁住读写锁,都可以调用pthread_rwlock_unlock进行解锁。

#include <pthread . h>
int pthread_rwlock_rdlock(pthread_rwlock_ t *rwlock) ;
int pthread_rwlock_wrlock(pthread_rwlock_ t *rwlock) ;
int pthread_rwlock_unlock(pthread_rwlock_ t *rwlock) ;
返回值:若成功则返回0,否则返回错误编号

4、条件变量
        条件变量本身不是锁,但它可以造成线程阻塞,给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。
      条件变量用pthread_cond_t数据类型来表示,在使用条件变量之前,必须首先对它进行初始化,可以用两种方式进行初始化,可以把常量PTHREAD_COND_INITIALIZER赋给静态分配的条件变量,但是如果条件变量是动态分配的,可以使用pthread_ co nd_init函数进行初始化,如果动态地分配条件变量,需要在释放内存前需要调用pthread_cond_destroy。

#include <pthread. h>
int pthread_cond_init(pthread_cond_t *restrict cond,pthread_condattr_ t *restrict attr);
int pthread_cond_destroy(pthread_cond t *cond) ;
返回值:若成功则返回0,否则返回错误编号

        将attr设置为NULL表示使用默认的属性初始化条件变量。
        使用pthread_cond_wait等待条件变为真,如果在给定的时间内条件不能满足,那么会生成一个代表出错码的返回变量。

#include <pthread.h>
int pthread_cond_wait (pthread_cond_ t *restrict cond,pthread mutex_t *restrict mutex) ;
int pthread_cond_timedwait (pthread_cond_t *restrict cond,pthread mutex_t *restrict mutex ,const struct timespec *restrict timeout) ;
返回值:若成功则返回0,否则返回错误编号

        传递给pthread_cond_wait的互斥量对条件进行保护,调用者把锁住的互斥量传给函数。函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁,这两个操作是原子操作。这样就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何变化。pthread_cond_wait返回时,互斥量再次被锁住。
        pthread_cond_timedwait函数的工作方式与pthread_cond_wait函数相似,只是多了一个timeout,timeout值指定了等待的时间。如果时间值到了但是条件还是没有出现,pthread_cond_timedwait将重新获取互斥量,然后返回错误ETIMEDOUT。
        有两个函数可以用于通知线程条件已经满足,pthread_cond_signal函数将唤醒等待该条件的某个线程,而pthread_cond_ broadcast函数将唤醒等待该条件的所有线程。

#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond) ;
返回值:若成功则返回0,否则返回错误编号

      调用pthread_cond_signal或者pthread_cond_broadcast,也称为向线程或条件发送信号,必须注意一定要在改变条件状态以后再给线程发送信号。
 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值