Linux中读写锁详细介绍
读写锁介绍
Linux 中的读写锁(Read-Write Lock)是一种用于线程同步的机制,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。这种机制在读操作远多于写操作的场景下,可以显著提高并发性能。读写锁主要有以下特点:
读写锁区分了对共享资源的读取和写入操作。这使得它可以根据不同的操作类型,采用不同的锁定策略。
对于读锁(共享)来说,允许多个线程同时持有,用于保护读取操作,当有线程持有读锁时,其他线程也可以获取读锁。
对于写锁(独占)来说,一次只能有一个线程持有,用于保护写入操作,当有线程持有写锁时,其他线程都不能获取读锁或者写锁。
想要获取读锁和写锁的要求:
一个线程想要成功获取某资源的读锁,要求其他线程不持有任何锁或者只持有读锁可以获取成功,如果其他线程持有该资源写锁的话就获取不成功。
一个线程想要成功获取某资源的写锁,要求其他线程不持有任何锁才可以获取成功,如果其他线程持有该资源写锁或者该资源读锁的话就获取不成功。
读写锁的适用情况
读写锁的优点:
- 提高读操作并发性:在读多写少的场景下,读写锁可以显著提高读取操作的并发性,从而提高程序的整体性能。
- 减少线程阻塞:与互斥锁相比,读写锁减少了线程因读取操作而阻塞的概率,这提高了线程的利用率和程序的响应速度。
读写锁的适用情况:
- 读多写少的共享资源:
- 例如,配置文件、数据缓存、查找表等。
- 需要频繁读取但很少修改的数据:
- 例如,传感器数据、实时监控数据等。
互斥锁VS读写锁互斥锁确保了在任何时刻只有一个线程可以访问受保护的资源,无论是读取还是写入。
读写锁的适合使用在在读操作远多于写操作的场景下,它允许多个线程同时读取共享资源,提高了读操作的并发性,但只允许一个线程写入,对于资源写操作没有提高。
读写锁的使用流程
- 初始化读写锁: 使用
pthread_rwlock_init()
创建读写锁。 - 获取锁:
读取共享资源前,使用pthread_rwlock_rdlock()获取读锁。
写入共享资源前,使用pthread_rwlock_wrlock()获取写锁。
- 访问共享资源: 在读写锁的保护下,安全地访问共享资源。
- 解锁读写锁: 在完成共享资源访问后,使用
pthread_rwlock_unlock()
解锁读写锁。 - 销毁读写锁: 在读写锁不再使用时,使用
pthread_rwlock_destroy()
销毁它。
pthread_rwlock_init():初始化读写锁
函数作用:初始化一个读写锁。
函数原型:int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
参数:
- rwlock:指向要初始化的读写锁变量的指针。
- attr:读写锁属性,通常设置为NULL表示使用默认属性。
返回值:
成功返回0,失败返回错误码。
注意:
- 为了让多个线程都能使用同一个读写锁,读写锁变量必须定义于线程可以共享访问的内存区域,比如全局变量静态变量或者堆上,读写锁变量的类型为pthread_rwlock_t。
- 使用读写锁的第一步必须是初始化读写锁,不初始化可能会出现未定义情况。
- 通常来说pthread_rwlock_init函数的第二个参数attr我们会设置为NULL,此时会创建一个默认属性的读写锁,完全已经够用。
pthread_rwlock_rdlock():获取读锁
函数作用:获取读锁。
函数原型:int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
参数:
rwlock:指向要获取读锁的读写锁变量的指针。
返回值:
成功返回0,失败返回错误码。
注意:
- 如果当前没有线程持有写锁,调用线程将立即获得读锁。
- 如果当前有线程持有写锁,调用线程将被阻塞,直到写锁被释放。
pthread_rwlock_tryrdlock():尝试获取读锁
函数作用:尝试获取读锁,如果读写锁已被写锁定,则不阻塞,立即返回错误。
函数原型:int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
参数:
rwlock:指向要尝试获取读锁的读写锁变量的指针。
返回值:
- 成功锁定返回0。
- 如果读写锁已被写锁定,返回
EBUSY
。 - 其他错误返回相应的错误码。
注意:
- pthread_rwlock_tryrdlock()是非阻塞的,它不会使调用线程进入等待状态。
- 这个函数在需要非阻塞的锁定操作时很有用。
pthread_rwlock_wrlock():获取写锁
函数作用:获取写锁。
函数原型:int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
参数:
rwlock:指向要获取写锁的读写锁变量的指针。
返回值:
成功返回0,失败返回错误码。
注意:
- 如果当前没有线程持有读锁或写锁,调用线程将立即获得写锁。
- 如果当前有线程持有读锁或写锁,调用线程将被阻塞,直到所有锁都被释放。
pthread_rwlock_trywrlock():尝试获取写锁
函数作用:尝试获取写锁,如果读写锁已被读锁定或写锁定,则不阻塞,立即返回错误。
函数原型:int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
参数:
rwlock:指向要尝试获取写锁的读写锁变量的指针。
返回值:
- 成功锁定返回0。
- 如果读写锁已被读锁定或写锁定,返回
EBUSY
。 - 其他错误返回相应的错误码。
注意:
- pthread_rwlock_trywrlock()是非阻塞的,它不会使调用线程进入等待状态。
- 这个函数在需要非阻塞的锁定操作时很有用。
pthread_rwlock_unlock():解锁读写锁
函数作用:解锁读写锁
函数原型:int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
参数:
rwlock:指向要解锁的读写锁变量的指针。
返回值:
成功返回0,失败返回错误码。
注意:
- 解锁读写锁后,其他等待该读写锁的线程可以获得锁。
- 持有读锁或写锁的线程都可以使用这个函数解锁。
一个线程对于同一个读写锁,在同一时间点上,只能拥有读锁或者写锁,不能同时拥有。
所以使用pthread_rwlock_unlock()进行解锁时,不需要指定解的是读锁还是写锁。操作系统内部会维护读写锁的状态,并且根据内部状态来确定如何解锁,这样设计简化了读写锁的使用
pthread_rwlock_destroy():销毁读写锁
函数作用:销毁一个读写锁。
函数原型:int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
参数:
rwlock:指向要销毁的读写锁变量的指针。
返回值:
成功返回0,失败返回错误码。
注意:
- 在读写锁不再使用时,应该及时销毁它以释放资源。
- 只有未被任何线程锁定的读写锁才能被销毁。
读写锁使用示例
示例:共享数据缓存
假设我们有一个共享的数据缓存,多个线程需要读取缓存中的数据,而只有少数线程需要更新缓存。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#define NUM_READERS 5
#define NUM_WRITERS 2
// 共享数据缓存
int data_cache = 0;
// 读写锁
pthread_rwlock_t rwlock;
// 读线程函数
void *reader_thread(void *arg) {
int thread_id = *(int *)arg;
// 获取读锁
pthread_rwlock_rdlock(&rwlock);
// 读取共享数据
printf("Reader %d: Reading data_cache = %d\n", thread_id, data_cache);
sleep(1); // 模拟耗时读取操作
// 释放读锁
pthread_rwlock_unlock(&rwlock);
pthread_exit(NULL);
}
// 写线程函数
void *writer_thread(void *arg) {
int thread_id = *(int *)arg;
// 获取写锁
pthread_rwlock_wrlock(&rwlock);
// 更新共享数据
data_cache++;
printf("Writer %d: Updated data_cache = %d\n", thread_id, data_cache);
sleep(2); // 模拟耗时写入操作
// 释放写锁
pthread_rwlock_unlock(&rwlock);
pthread_exit(NULL);
}
int main() {
pthread_t readers[NUM_READERS];
pthread_t writers[NUM_WRITERS];
int reader_ids[NUM_READERS];
int writer_ids[NUM_WRITERS];
// 初始化读写锁
if (pthread_rwlock_init(&rwlock, NULL) != 0) {
perror("pthread_rwlock_init");
exit(EXIT_FAILURE);
}
// 创建读线程
for (int i = 0; i < NUM_READERS; i++) {
reader_ids[i] = i;
if (pthread_create(&readers[i], NULL, reader_thread, &reader_ids[i]) != 0) {
perror("pthread_create (reader)");
exit(EXIT_FAILURE);
}
}
// 创建写线程
for (int i = 0; i < NUM_WRITERS; i++) {
writer_ids[i] = i;
if (pthread_create(&writers[i], NULL, writer_thread, &writer_ids[i]) != 0) {
perror("pthread_create (writer)");
exit(EXIT_FAILURE);
}
}
// 等待读线程结束
for (int i = 0; i < NUM_READERS; i++) {
pthread_join(readers[i], NULL);
}
// 等待写线程结束
for (int i = 0; i < NUM_WRITERS; i++) {
pthread_join(writers[i], NULL);
}
// 销毁读写锁
if (pthread_rwlock_destroy(&rwlock) != 0) {
perror("pthread_rwlock_destroy");
exit(EXIT_FAILURE);
}
return 0;
}
代码解释:
- 包含头文件:
stdio.h
:用于输入输出。stdlib.h
:用于标准库函数。pthread.h
:用于线程相关函数。unistd.h
:用于sleep()
函数。
- 定义共享数据和读写锁:
data_cache
:一个整数变量,作为共享数据缓存。rwlock
:一个pthread_rwlock_t
类型的变量,用于读写锁。
- 读线程函数
reader_thread()
:- 每个读线程执行这个函数。
- 首先,使用
pthread_rwlock_rdlock()
获取读锁。 - 然后,读取共享数据
data_cache
。 - 使用
sleep()
模拟耗时读取操作。 - 最后,使用
pthread_rwlock_unlock()
释放读锁。
- 写线程函数
writer_thread()
:- 每个写线程执行这个函数。
- 首先,使用
pthread_rwlock_wrlock()
获取写锁。 - 然后,更新共享数据
data_cache
。 - 使用
sleep()
模拟耗时写入操作。 - 最后,使用
pthread_rwlock_unlock()
释放写锁。
main()
函数:- 初始化读写锁:使用
pthread_rwlock_init()
初始化读写锁。 - 创建读线程:创建多个读线程,并传递线程ID作为参数。
- 创建写线程:创建多个写线程,并传递线程ID作为参数。
- 等待线程结束:使用
pthread_join()
等待所有线程结束。 - 销毁读写锁:使用
pthread_rwlock_destroy()
销毁读写锁。
- 初始化读写锁:使用
程序输出显示读线程并发读取共享数据的过程,以及写线程更新共享数据的过程。由于读写锁的保护,读线程可以并发执行,而写线程的更新操作是互斥的。