读写锁和自旋锁
- 1 读者写者问题
- 2 读写锁
- 3 读写锁的两大特性
1 读者写者问题
读者写者是一种生产消费模型,所以就满足"321"原则:
- 三种关系:生产与消费,生产与生产,消费与消费
- 两种角色:生产者与消费者
- 一个交易场所:临界资源
在读者写者问题中,读者与读者是并发的,不同读者之间不会互相影响,因为只是访问数据,并不会读数据进行修改。写者与写者是互斥的,临界资源只能让一个写者进行书写。读者与写者的关系比较复杂,是互斥与同步,读写不能同时进行,读完了要与写进行同步,写完了要与读同步。
一般而言:读者写者模型中读者很多,写者很少。
2 读写锁
读写锁的逻辑可以这么理解:
- 首先需要一个互斥锁,来对写者进行上锁。保证写者与写者之间的互斥关系
- 然后对应读者来说,他们是并发执行的,为了可以保证读完了可以进行写的同步,需要一个计数器来记录读者的数量。
- 有了这个计数器,那么就相当于读者都会访问这个计数器,所以需要锁来进行保护。
- 当进入读者时,先将将计数器锁获取。然后在对计数器进行++,再进行解锁,然后,写锁获取,让写者无法获取锁阻塞 ,进行读操作。之后在将计数器锁获取进行–,再进行解锁
- 当进入写者时,将写者锁获取,之后进行写操作,最后进行解锁。
这是读写锁的逻辑,当实际中线程库为我们提供了专门的读写锁,我们不需要使用互斥锁来进行模拟!
#include <pthread.h>
//销毁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
//初始化
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
//读者锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
//写者锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
//解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
使用方法和互斥锁很类似。
由于读写是互斥的,读者多的情况下就可能导致造成写者饥饿问题:
我们编写一个简单的程序实现读写锁:
#include <pthread.h>
#include <iostream>
#include <vector>
#include <stdlib.h>
#include <unistd.h>
// 进行访问的全局数据
int Data = 0;
// 读写锁
pthread_rwlock_t rwlock;
// 读操作
void *read(void *args)
{
int id = *(int *)args;
//sleep(1);
while (true)
{
// 读者上锁
pthread_rwlock_rdlock(&rwlock);
// 进行读操作
std::cout << "读者线程-" << id << "正在读取数据:" << Data << std::endl;
sleep(1);
// 完成写操作
// 解锁
pthread_rwlock_unlock(&rwlock);
}
delete (int*)args;
return nullptr;
}
// 写操作
void *write(void *args)
{
int id = *(int *)args;
while (true)
{
// 读者锁
pthread_rwlock_wrlock(&rwlock);
// 写操作
Data = rand() % 100;
std::cout << "写者线程-" << id << "正在写入数据:" << Data << std::endl;
sleep(1);
// 解锁
pthread_rwlock_unlock(&rwlock);
}
delete (int*)args;
return nullptr;
}
int main()
{
// 读者写者数量
int write_count = 2;
int read_count = 2;
std::vector<pthread_t> wthreads(write_count, 0);
std::vector<pthread_t> rthreads(read_count, 0);
pthread_rwlock_init(&rwlock, nullptr);
srand(time(nullptr));
for (int i = 0; i < read_count; i++)
{
int *id = new int(i);
pthread_create(&rthreads[i], nullptr, read, id);
}
for (int i = 0; i < write_count; i++)
{
int *id = new int(i);
pthread_create(&wthreads[i], nullptr, write, id);
}
for (int i = 0; i < read_count; i++)
{
pthread_join(rthreads[i], nullptr);
}
for (int i = 0; i < write_count; i++)
{
pthread_join(wthreads[i], nullptr);
}
pthread_rwlock_destroy(&rwlock);
return 0;
}
运行会发现:
写者根本进不来,只有读者在进行,这是因为这里读者读到数据没有进行处理,而是连续的再进行读取,这就导致写者没有机会获取到全局变量,就不能进行写操作。我们可以加入sleep(1)
模拟处理数据:这样写者就有机会获取到全局变量进行处理了!!!
3 读写锁的两大特性
在生产者消费者模型中,消费者与生产者的关系是对等的。但在读者写者问题中,读者与写者的关系不对等。一般会有两种策略:
- 读者优先(Reader-Preference)
在这种策略中,系统会尽可能多地允许多个读者同时访问资源(比如共享文件或数据),而不会优先考虑写者。这意味着当有读者正在读取时,新到达的读者会立即被允许进入读取区,而写者则会被阻塞,直到所有读者都离开读取区。读者优先策略可能会导致写者饥饿(即写者长时间无法获得写入权限),特别是当读者频繁到达时。
读者优先的实际应用场景:
- 文档数据库:
在文档数据库中,通常读取操作远多于写入操作。采用读者优先策略可以最大化读取效率,让多个用户同时读取文档而不会相互阻塞。例如,一个在线百科全书网站,用户频繁读取词条内容,但编辑更新的频率相对较低。 - 配置文件读取:
在多线程应用中,配置文件通常会被频繁读取但很少写入。使用读者优先的读写锁可以保证配置文件在更新时不会影响大量读取操作。 - 缓存系统:
缓存系统中的数据读取非常频繁,而写入(缓存失效或更新)相对较少。读者优先策略可以保证缓存数据的快速访问。
其潜在问题就是会造成写者饥饿:如果写者操作不频繁,但读者操作非常频繁,写者可能长时间无法获得锁,导致写入操作被无限期延迟。
- 写者优先(Writer-Preference)
在这种策略中,系统会优先考虑写者。当写者请求写入权限时,系统会尽快地让写者进入写入区,即使此时有读者正在读取。这通常意味着一旦有写者到达,所有后续的读者都会被阻塞,直到写者完成写入并离开写入区。写者优先策略可以减少写者等待的时间,但可能会导致读者饥饿(即读者长时间无法获得读取权限),特别是当写者频繁到达时。
写者优先的实际应用场景:
- 实时数据系统:
在需要实时更新和读取数据的应用中,写者优先策略可以确保数据的实时性。例如,股票市场信息需要实时更新,并且更新必须尽快反映给所有用户。 - 状态更新:
在某些系统中,状态的更新(写入操作)需要被尽快处理以保证系统的正确性和一致性。例如,游戏状态更新需要及时反映给所有玩家。 - 日志系统:
在日志系统中,写入操作是连续的,且重要性高于读取操作。写者优先可以确保日志记录不会因为读取操作而延迟。
写者优先的潜在问题是会造成读者饥饿:如果写者操作非常频繁,读者可能会长时间无法获得锁,导致读取操作被阻塞。
总之,读者优先适合读取操作远多于写入操作的场景,可以最大化读取效率。写者优先适合写入操作的重要性高于读取操作的场景,可以确保写入操作的及时性