1.背景概念
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。这样就衍生出了读者写者模型,在这个模型中,有两类线程:读者和写者。读者线程读取共享资源,而写者线程则修改共享资源,读者和读者之间为共享关系,写者和写者为同步互斥关系,读者和写者之间为互斥关系。
该模型有两种策略:
读者优先策略
在读者优先策略中,允许多个读者同时访问共享资源,有读者在读时,写者必须等待,直到所有当前的读者和写者都完成访问。这种策略可以提高并发度,但可能导致写者饥饿。
写者优先策略
在写者优先策略中,一旦有写者等待,新的读者必须等待,直到所有当前的读者和写者都完成访问。这种策略可以减少写者的等待时间,但可能导致读者饥饿。
通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 在Linux系统中,可以使用信号量(Semaphore)或读写锁(Read-Write Lock)来解决读者写者问题。信号量可以用来实现互斥访问,而读写锁则可以允许多个读者同时访问,但在有写者等待时,新的读者必须等待,接下来我们介绍读写锁。
2.读写锁
- 读模式(共享模式)
- 多个线程可以同时以读模式持有读写锁。当一个线程以读模式获取到读写锁后,其他线程如果也想要以读模式获取该锁,是可以成功获取的。这是因为多个读操作通常不会相互干扰,不会破坏数据的一致性。例如,多个线程同时读取一个共享文件的内容,它们可以同时进行,不会有冲突。
- 写模式(独占模式)
- 只有一个线程可以以写模式持有读写锁。当一个线程以写模式获取到读写锁时,其他任何线程(无论是想要以读模式还是写模式获取该锁)都必须等待。这是因为写操作会修改共享资源,如果有其他线程同时进行读或写操作,可能会导致数据不一致。例如,当一个线程正在修改一个共享文件时,不允许其他线程同时读取或修改该文件。
在Linux中,读写锁的类型为 pthread_rwlock_t,其使用方法与互斥锁基本一致。
读写锁的创建
1. 可以使用PTHREAD_RWLOCK_INITIALIZER
初始化创建全局的读写锁,并且无需销毁。
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
2. 使用 pthread_rwlock_init 函数初始化读写锁。
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
参数:
rwlock
:指向要初始化的读写锁对象的指针。attr
:指向读写锁属性对象的指针,如果不需要特殊属性,可以设置为NULL
,使用默认属性。
函数成功时返回 0;失败时返回错误码。
加锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
pthread_rwlock_rdlock 用于给读写锁申请一个读锁。如果读写锁当前没有被写锁占用,那么调用线程可以立即获得读锁。如果读写锁当前被写锁占用,那么调用线程将被阻塞,直到写锁被释放。
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
pthread_rwlock_wrlock 用于给读写锁申请一个写锁。如果读写锁当前没有被任何锁占用,那么调用线程可以立即获得写锁。如果读写锁当前被读锁或写锁占用,那么调用线程将被阻塞,直到所有的读锁和写锁都被释放。
rwlock
:指向要申请读锁的读写锁对象的指针。
解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
pthread_rwlock_unlock
函数用于释放之前通过 pthread_rwlock_rdlock
(读锁)或者 pthread_rwlock_wrlock
(写锁)获取的读写锁。
读写锁的销毁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
3.基于读写锁实现的读者写者模型
基于读写锁,我们来实现一个测试代码看看效果
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
using namespace std;
int g_tickets = 100;
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; // 读写锁
struct ThreadData
{
pthread_t tid;
string name;
};
void *ReaderRoutinue(void *arg)
{
ThreadData *reader = static_cast<ThreadData *>(arg);
while (true)
{
usleep(1000);
pthread_rwlock_rdlock(&rwlock); // 读锁
if (g_tickets > 0)
cout << reader->name << " check a ticket-" << g_tickets << endl;
else
{
pthread_rwlock_unlock(&rwlock);
break;
}
pthread_rwlock_unlock(&rwlock);
}
return nullptr;
}
void *WriterRoutinue(void *arg)
{
ThreadData *writer = static_cast<ThreadData *>(arg);
while (true)
{
usleep(2000);
pthread_rwlock_wrlock(&rwlock); // 写锁
if (g_tickets > 0)
cout << writer->name << " get a ticket-" << g_tickets-- << endl;
else
{
pthread_rwlock_unlock(&rwlock);
break;
}
pthread_rwlock_unlock(&rwlock);
}
return nullptr;
}
int main()
{
ThreadData readers[5];
ThreadData writers[2];
for (int i = 0; i < 5; i++)
{
readers[i].name = "Reader-" + to_string(i + 1);
pthread_create(&readers[i].tid, nullptr, ReaderRoutinue, &readers[i]);
}
for (int i = 0; i < 2; i++)
{
writers[i].name = "Writer-" + to_string(i + 1);
pthread_create(&writers[i].tid, nullptr, WriterRoutinue, &writers[i]);
}
for (int i = 0; i < 5; i++)
pthread_join(readers[i].tid, nullptr);
for (int i = 0; i < 2; i++)
pthread_join(writers[i].tid, nullptr);
return 0;
}