读者写者问题,单例模式,自旋锁
Posted 两片空白
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了读者写者问题,单例模式,自旋锁相关的知识,希望对你有一定的参考价值。
目录
一.读者写者问题
1.1 概念
读者和写者问题,描述的就是有些公共数据修改的机会比较少,读的机会反而很多。在编写多线程时,这种情况时非常常见的。比如:登录账户,每次登录的时候都在不断的读取后台数据库中保存的账户和密码信息,但是用户很少会修改。再比如:看新闻,一条新闻会有很多用户浏览,但是很少会有人修改。
通常情况下,再读的过程中,往往伴随着查找,中间耗时比较长。如果给这种代码加上互斥锁,会极大的降低我们程序的效率。但是又一种锁可以专门解决读多写少的情况,叫做读写锁。
1.2 读者和写者的关系
相比较生产者和消费者模型。生产者和生产者之间互斥,消费者和消费者之间互斥,生产者和消费者之间互斥且同步。
读者和写者,写者和写者之间互斥,读者和写者之间互斥且同步,读者和读者之间共享。读者和读者之间可以共享临界资源。
读者和写者相比较于消费者和生产者之间为什么会有不同?
因为读者不会拿任务,只会读取任务,消费者会拿任务。
1.3 读写锁接口
读写锁的优先级:
- 设置读写锁优先级的接口
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
pref有三种选择:
PTHREAD_RWLOCK_PREFER_READER_NP:(默认设置)读优先,意思就是当没有线程占领锁时,读者优先占领锁,当写者占领锁,使用完后,优先让读者占领锁。反正就是让读者线程优先占领读写锁。但是这样会导致写者饥饿问题,由于读者线程多,导致写者长时间占领不到锁。
PTHREAD_RWLOCK_PREFER_WRITER_NP:写优先。让写者线程优先占领读写锁。目前有BUG导致和读者优先表现一致。
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP:写者优先,但是写者不能递归加锁。读者写者谁先占到,谁拥有锁。
- 初始化读写锁
#include<pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
参数:rwlock:要初始化的读写锁
attr:属性
- 销毁读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
参数:要销毁的读写锁
- 加锁和解锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);//写者加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);//读者加锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);//解锁
1.4 实现读优先的伪代码
写加锁,读加锁实现伪代码。
//读优先
mutex r,w;//定义两把锁
int rc = 0;//读者计数器
//读者加锁
pthread_rwlock_rdlock(){
lock(r);//rc时共享资源,要加上的锁
rc+=1;
if(rc==1){
lock(w);//因为读写互斥,第一个读者进来就加锁,申请失败会阻塞等待
}
unlock(r);
//读者共享,不需要加锁
//读操作
lock(r);
rc-=1;
if(rc==0){
unlock(w);//最后一个要解写者锁
}
unlock(r);
}
//写者加锁
pthread_rwlock_wrlock(){
//写者之间互斥
lock(w);
//写操作
unlock(w);
}
二.单例模式
2.1 什么是设计模式
设计模式就是一些大佬在编写代码的过程中,针对一些经典常见场景,给定对应解决方案,于是将其设计成一种模式,以后我们想使用就只需要套这个模式就好了。
2.2 单例模式介绍
某些类,只应该具有一个对象(实例化),称之为单例。
在很多服务器开发场景中,经常需要让服务器加载很多数据到内存中,此时往往需要单例的类来管理这些数据。
2.3 饿汉实现方式和懒汉实现方式
说明:static修饰的对象当类被加载到内存就被定义出来了,而不是等到类实例化对象后才定义。static修饰的成员在类中只有一份。
饿汉和懒汉式通过static修饰的成员属于整个类,不管类实例化多少对象,都只有一个静态成员。
- 饿汉实现方式
饿汉实现方式就是:当类加载到到内存,就将成员变量定义出来了。
template<class T>
class Singleton{
static T data;//类加载到内存直接定义
public:
//获得data
static T* GetInstance(){
return &data;
}
}
- 懒汉实现方式
懒汉实现方式就是,类加载时并没有将对象定义出来,而是在需要时才定义。
template<class T>
class Singleton(){
//类加载时只是定义一个指针,
static T* inst;
public:
//当需要inst时,才会定义
static T* GetInstance(){//不需要this指针,inst属于整个类。是静态成员
if(inst == nullptr){
inst = new T();
}
return inst;
}
}
饿汉实现方式会比懒汉实现方式启动得慢,因为饿汉实现方式在启动时就将需要的成员全部定义出来了。懒汉将定义时间挪后了。
饿汉模式在使用时不会出现线程安全问题,但是在懒汉模式在使用时可能会出现线程安全问题。
在多线程中,当两个线程同时调用GetInstance时,可能会创建出两个inst成员。
- 懒汉模式线程安全版本
template<class T>
class Singleton(){
//类加载时只是定义一个指针,
volatile static T* inst;
static Std::mutex lock;
public:
//当需要inst时,才会定义
static T* GetInstance(){//不需要this指针,inst属于整个类。是静态成员
if(inst == nullptr){//双重判定,降低锁的冲突概率,提高性能
lock.lock(); //加锁,保证只由一个线程可以进入
if(inst == nullptr){
inst = new T();
}
lock_unlock();
}
return inst;
}
}
注意事项:
- 加锁位置,inst是临界资源,只需要对其加锁。
- 两重判定。不一定每一个线程就来inst都为空,不至于每一个线程都加锁等待。
- volatile,防止编译器优化。
三.自旋锁
3.1 概念
先比较互斥锁,当一个线程占用锁在访问临界资源时,其它线程申请锁失败,只能阻塞等待。
自旋锁,由于占有锁的线程,访问临界资源的时间很短,其它线程想要申请临界锁,申请锁失败,不会阻塞等待,而是会不断检测锁的状态。
实现伪代码:
lock:
movb $0 %al
xchgb %al mutex
if(al寄存器内容>0){
return 0;//申请锁成功
}
else{
goto lock;//再去检测
}
需要使用自旋锁还是互斥锁,取决于线程访问临界资源时间长短。
3.2 接口函数
头文件 #include<pthread.h>
自旋量:pthread_spinlock_t spin
- 初始化
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
- 销毁
int pthread_spin_destroy(pthread_spinlock_t *lock);
- 加锁和解锁
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
以上是关于读者写者问题,单例模式,自旋锁的主要内容,如果未能解决你的问题,请参考以下文章