【Linux多线程同步】读写锁

本文详细介绍了Linux下的读写锁概念、特点及其应用场景,包括动态初始化、属性设置、使用方法等,并通过实例对比了读写锁与互斥量的区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

文章参考自:Linux线程同步之读写锁(rwlock)(anonymalias的专栏)


读写锁和互斥量(互斥锁)很类似,是另一种线程同步机制,但不属于POSIX标准,可以用来同步同一进程中的各个线程。当然如果一个读写锁存放在多个进程共享的某个内存区中,那么还可以用来进行进程间的同步,

和互斥量不同的是:互斥量会把试图进入已保护的临界区的线程都阻塞;然而读写锁会视当前进入临界区的线程和请求进入临界区的线程的属性来判断是否允许线程进入。

相对互斥量只有加锁和不加锁两种状态,读写锁有三种状态:读模式下的加锁,写模式下的加锁,不加锁

读写锁的使用规则:

  • 只要没有写模式下的加锁,任意线程都可以进行读模式下的加锁;
  • 只有读写锁处于不加锁状态时,才能进行写模式下的加锁;

读写锁也称为共享-独占(shared-exclusive)锁,当读写锁以读模式加锁时,它是以共享模式锁住,当以写模式加锁时,它是以独占模式锁住。读写锁非常适合读数据的频率远大于写数据的频率从的应用中。这样可以在任何时刻运行多个读线程并发的执行,给程序带来了更高的并发度。



1、读写锁的动态初始化和销毁

/* Initialize read-write lock  */  
int pthread_rwlock_init (pthread_rwlock_t *__restrict __rwlock,  
				__const pthread_rwlockattr_t *__restrict __attr);  
  
/* Destroy read-write lock */  
int pthread_rwlock_destroy (pthread_rwlock_t *__rwlock);  

//返回值:成功返回0,否则返回错误代码  
pthread_rwlock_init()  第二个参数是读写锁的属性,如果采用默认属性,可以传入空指针 NULL 。在释放读写锁占用的内存之前,需要调用  pthread_rwlock_destroy()  进行销毁读写锁占用的资源。


和互斥量,条件变量一样,如果读写锁是静态分配的,可以通过常量进行初始化,如下:

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

2、读写锁的属性设置

/* 初始化读写锁属性对象 */  
int pthread_rwlockattr_init (pthread_rwlockattr_t *__attr);  
  
/* 销毁读写锁属性对象 */  
int pthread_rwlockattr_destroy (pthread_rwlockattr_t *__attr);  
  
/* 获取读写锁属性对象在进程间共享与否的标识*/  
int pthread_rwlockattr_getpshared (__const pthread_rwlockattr_t * __restrict __attr,  
                                          int *__restrict __pshared);  
  
/* 设置读写锁属性对象,标识在进程间共享与否  */  
int pthread_rwlockattr_setpshared (pthread_rwlockattr_t *__attr, int __pshared);  
  
//返回值:成功返回0,否则返回错误代码
这些设置和互斥锁的设置类似。 见: http://blog.youkuaiyun.com/anonymalias/article/details/9174403 .

3、读写锁的使用

/* 读模式下加锁  */  
int pthread_rwlock_rdlock (pthread_rwlock_t *__rwlock);  
  
/* 非阻塞的读模式下加锁  */  
int pthread_rwlock_tryrdlock (pthread_rwlock_t *__rwlock);  
  
# ifdef __USE_XOPEN2K  
/*  限时等待的读模式加锁 */  
int pthread_rwlock_timedrdlock (pthread_rwlock_t *__restrict __rwlock,  
                                       __const struct timespec *__restrict __abstime);  
# endif  
  
/* 写模式下加锁  */  
int pthread_rwlock_wrlock (pthread_rwlock_t *__rwlock);  
  
/* 非阻塞的写模式下加锁 */  
int pthread_rwlock_trywrlock (pthread_rwlock_t *__rwlock);  
  
# ifdef __USE_XOPEN2K  
/* 限时等待的写模式加锁 */  
int pthread_rwlock_timedwrlock (pthread_rwlock_t *__restrict __rwlock,  
                                       __const struct timespec *__restrict __abstime);  
# endif  
  
/* 解锁 */  
int pthread_rwlock_unlock (pthread_rwlock_t *__rwlock);  
  
//返回值:成功返回0,否则返回错误代码

1pthread_rwlock_rdlock()系列函数

pthread_rwlock_rdlock()用于以读模式即共享模式获取读写锁,如果读写锁已经被某个线程以写模式占用,那么调用线程就被阻塞。在实现读写锁的时候可以对共享模式下锁的数量进行限制(目前不知如何限制)。

pthread_rwlock_tryrdlock()pthread_rwlock_rdlock()的唯一区别就是,在无法获取读写锁的时候,调用线程不会阻塞,会立即返回,并返回错误代码EBUSY

pthread_rwlock_timedrdlock()是限时等待读模式加锁,时间参数struct timespec * __restrict __abstime也是绝对时间,和条件变量的pthread_cond_timedwait()使用基本一致。


2pthread_rwlock_wrlock()系列函数

pthread_rwlock_wrlock()用于写模式即独占模式获取读写锁,如果读写锁已经被其他线程占用,不论是以共享模式还是独占模式占用,调用线程都会进入阻塞状态。

pthread_rwlock_trywrlock()在无法获取读写锁的时候,调用线程不会被阻塞,会立即返回,并返回错误代码EBUSY

pthread_rwlock_timedwrlock()是限时等待写模式加锁,也和条件变量的pthread_cond_timedwait()使用基本一致。


3pthread_rwlock_unlock()

无论以共享模式还是独占模式获得的读写锁,都可以通过调用pthread_rwlock_unlock()函数进行释放该读写锁。


以下是测试代码,目的是实现这样一个功能。一共有两种线程,一个 csi_rs 线程,多个 data_tx 线程。多个data_tx 需要从 csi_rs 读取数据,而这部分数据每过一段时间都会被修改(由于数据的修改花费时间较长,我们可以开辟两个空间,一个用于修改,一个用于使用,修改成功后两者交换),在新数据尚未被计算出来之前,使用旧数据。注意:要保证同一批次 data_tx 使用同一个整数,不能让有的 data_tx 使用新的,有的用旧的以下的代码将数据简化为一个整数,每过一段时间就会加一。


使用读写锁后发现,写者被饿死。因为读写锁的读者优先级高于写者,如果有接连不断的读者,那么写者将会被饿死。如下:

#include <pthread.h>
#include <unistd.h>
#include <iostream>
using namespace std;

int *data, *first, *second, *ptr;
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

void* csi_rs(void *arg)
{
	ptr = first;
	while(1)
	{
		ptr == first ? (*second = *first + 1) : (*first = *second + 1);
		usleep(20000);

		pthread_rwlock_wrlock(&rwlock);
		cout << "Once I came here." << endl;
		ptr = (ptr == first ? second : first);
		pthread_rwlock_unlock(&rwlock);
	}
}

void* data_tx(void *arg)
{	
	int count = 10;

	while(count--)
	{
		cout << "count = " << count << endl;
		pthread_rwlock_rdlock(&rwlock);
		cout << pthread_self() << "  " << *ptr << endl;
		usleep(1000);
		pthread_rwlock_unlock(&rwlock);
	}
}	

int main()
{
	data = new int[2];
	data[0] = 0;
	first = data;
	second = data + 1;

	pthread_t pid[5];
	pthread_create(pid, NULL, csi_rs, NULL);
	sleep(1);
	for(int i = 1; i < 5; ++i)
	{
		pthread_create(pid + i, NULL, data_tx, NULL);
	}

	for(int i = 1; i < 5; ++i)
	{
		pthread_join(pid[i], NULL);
	}
	cout << "Over" << endl;
	cout << data[0] << data[1] << endl;

	return 0;
}


以上代码运行结果可以发现,当 data_tx 开始运行后, ptr就再也没有更新过,就是因为写者被饿死了。


google了网上写优先的读写锁,发现将有可能导致多个 data_tx 线程在同一 count 下使用了不同的数据。为了避免该情况,我改变了实现途径,用条件变量实现。由于 data_tx 执行时间相对于 csi_rs 是很短的,所以只需要使用条件变量完成同一批次 data_tx 间同步,并在此时发信号让 csi_rs 修改 ptr 即可。

新的代码如下:

#include <pthread.h>
#include <unistd.h>
#include <iostream>
#include <errno.h>
using namespace std;

int *data, *first, *second, *ptr;

int cnt = 0;
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond1 = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond2 = PTHREAD_COND_INITIALIZER;

int stick_this_thread_to_core(int core_id)
{
	int num_cores = sysconf(_SC_NPROCESSORS_ONLN);
	if (core_id < 0 || core_id >= num_cores)
		return EINVAL;
	
	cpu_set_t cpuset;
	CPU_ZERO(&cpuset);
	CPU_SET(core_id, &cpuset);

	pthread_t current_thread = pthread_self();
	return pthread_setaffinity_np(current_thread, sizeof(cpu_set_t), &cpuset);
}

void* csi_rs(void *arg)
{
	while(1)
	{
		ptr == first ? (*second = *first + 1) : (*first = *second + 1);
		usleep(20000);
	
		pthread_mutex_lock(&mutex1);
		pthread_cond_wait(&cond1, &mutex1);
		cout << "I'm changing the data." << endl;
		ptr = (ptr == first ? second : first);
		pthread_mutex_unlock(&mutex1);
	}
}

void* data_tx(void *arg)
{	
	int coreId = *(int *)arg;
	if(stick_this_thread_to_core(coreId))
	{
		cout << "Error" << endl;	
	}

	int count = 100;
	while(count--)
	{
		cout << "count = " << count << endl;
		cout << pthread_self() << "  " << *ptr << endl;
		usleep(1000);

		pthread_mutex_lock(&mutex2);
		cnt++;
		if(cnt == 4)
		{
			cnt = 0;
			pthread_cond_signal(&cond1);
			pthread_cond_broadcast(&cond2);
		}
		else
		{
			pthread_cond_wait(&cond2, &mutex2);
		}
		pthread_mutex_unlock(&mutex2);
		usleep(10);	//由于未对读 ptr 上锁,这里延时一段时间是为了让数据修改完毕后再进入新一轮读取,有待验证
	}
}	

int main()
{
	data = new int[2];
	data[0] = 0;
	first = data;
	second = data + 1;
	ptr = first;
	int coreId[4] = {0, 1, 2, 3};

	pthread_t pid[5];
	pthread_create(pid, NULL, csi_rs, NULL);
	usleep(2000);						//保证 data_tx 是在同一批次完成后发送 signal

	cout << "------------start-----------" << endl;
	for(int i = 1; i < 5; ++i)
	{
		pthread_create(pid + i, NULL, data_tx, coreId + i - 1);
	}
	for(int i = 1; i < 5; ++i)
	{
		pthread_join(pid[i], NULL);
	}
	cout << "------------end------------" << endl;

	cout << "data[0] = " << data[0] << '\n' << "data[1] = " << data[1] << endl;

	return 0;
}



代码中的延时是仿真数据处理时间,usleep()函数的入参单位是微秒。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值