当前位置: 首页 > news >正文

线程安全的单例模式与读者写者问题

什么是单例模式

单例模式是一种 "经典的, 常用的, 常考的" 设计模式.

什么是设计模式

大佬们针对一些经典的常见的场景, 给定了一些对应的解决方案, 这个就是设计模式

单例模式的特点

某些类, 只应该具有一个对象(实例), 就称之为单例. 例如一个男人只能有一个媳妇. 在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据.

饿汉实现方式和懒汉实现方式

吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭. 吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式.

懒汉方式最核心的思想是 "延时加载". 从而能够优化服务器的启动速度.

全局对象在加载的时候就有了,全局对象的生命周期随进程,如果全局对象定义太多就会拖慢饿汉模式加载速度

懒汉模式按需要加载,一切操作消耗不变但是将一整块加载时间分为多块

饿汉方式实现单例模式

template <typename T> 
class Singleton { static T data; 
public: static T* GetInstance() { return &data; } 
}; 

只要通过 Singleton 这个包装类来使用 T 对象, 则一个进程中只有一个 T 对象的实例.

懒汉方式实现单例模式

template <typename T> 
class Singleton { static T* inst; 
public: static T* GetInstance() { if (inst == NULL) { inst = new T(); } return inst; } 
}; 

存在一个严重的问题, 线程不安全. 第一次调用 GetInstance 的时候, 如果两个线程同时调用, 可能会创建出两份 T 对象的实例. 但是后续再次调用, 就没有问题了.

单例模式线程池(懒汉)

防止串行执行,大量线程判断为空先进去竞争锁,竞争到的才能去创建tp_,tp_存在后其他线程就没必要再去竞争锁判断是否为空了,不需要加\释放锁

static ThreadPool<T> *GetInstance(){if (nullptr == tp_) // ???{pthread_mutex_lock(&lock_);if (nullptr == tp_){std::cout << "log: singleton create done first!" << std::endl;tp_ = new ThreadPool<T>();}pthread_mutex_unlock(&lock_);}return tp_;}
#pragma once#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>struct ThreadInfo
{pthread_t tid;std::string name;
};static const int defalutnum = 5;template <class T>
class ThreadPool
{
public:void Lock(){pthread_mutex_lock(&mutex_);}void Unlock(){pthread_mutex_unlock(&mutex_);}void Wakeup(){pthread_cond_signal(&cond_);}void ThreadSleep(){pthread_cond_wait(&cond_, &mutex_);}bool IsQueueEmpty(){return tasks_.empty();}std::string GetThreadName(pthread_t tid){for (const auto &ti : threads_){if (ti.tid == tid)return ti.name;}return "None";}public:static void *HandlerTask(void *args){ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);std::string name = tp->GetThreadName(pthread_self());while (true){tp->Lock();while (tp->IsQueueEmpty()){tp->ThreadSleep();}T t = tp->Pop();tp->Unlock();t();std::cout << name << " run, "<< "result: " << t.GetResult() << std::endl;}}void Start(){int num = threads_.size();for (int i = 0; i < num; i++){threads_[i].name = "thread-" + std::to_string(i + 1);pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this);}}T Pop(){T t = tasks_.front();tasks_.pop();return t;}void Push(const T &t){Lock();tasks_.push(t);Wakeup();Unlock();}static ThreadPool<T> *GetInstance(){if (nullptr == tp_) // ???{pthread_mutex_lock(&lock_);if (nullptr == tp_){std::cout << "log: singleton create done first!" << std::endl;tp_ = new ThreadPool<T>();}pthread_mutex_unlock(&lock_);}return tp_;}private:ThreadPool(int num = defalutnum) : threads_(num){pthread_mutex_init(&mutex_, nullptr);pthread_cond_init(&cond_, nullptr);}~ThreadPool(){pthread_mutex_destroy(&mutex_);pthread_cond_destroy(&cond_);}ThreadPool(const ThreadPool<T> &) = delete;const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // a=b=c
private:std::vector<ThreadInfo> threads_;std::queue<T> tasks_;pthread_mutex_t mutex_;pthread_cond_t cond_;static ThreadPool<T> *tp_;static pthread_mutex_t lock_;
};template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;
#include <iostream>
#include <ctime>
#include "ThreadPool.hpp"
#include "Task.hpp"pthread_spinlock_t slock;int main()
{// pthread_spin_init(&slock, 0);// pthread_spin_destroy(&slock);// 如果获取单例对象的时候,也是多线程获取的呢?std::cout << "process runn..." << std::endl;sleep(3);// ThreadPool<Task> *tp = new ThreadPool<Task>(5);ThreadPool<Task>::GetInstance()->Start();srand(time(nullptr) ^ getpid());while(true){//1. 构建任务int x = rand() % 10 + 1;usleep(10);int y = rand() % 5;char op = opers[rand()%opers.size()];Task t(x, y, op);ThreadPool<Task>::GetInstance()->Push(t);//2. 交给线程池处理std::cout << "main thread make task: " << t.GetTask() << std::endl;sleep(1);}
}

其他常见的各种锁

悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行 锁等),当其他线程想要访问数据时,被阻塞挂起。

乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前, 会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。

CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不 等则失败,失败则重试,一般是一个自旋的过程,即不断重试。

自旋锁:

自旋锁(Spinlock)是一种忙等待(Busy-Waiting)锁,适用于短期临界区多核环境。与互斥锁(Mutex)不同,线程在获取自旋锁失败时不会阻塞,而是循环检查锁状态,直到成功获取锁。

不会挂起,而是由进程周而复始的申请锁(进入临界区的线程时间非常短)

#include <pthread.h>

int pthread_spin_lock(pthread_spinlock_t *lock);

int pthread_spin_trylock(pthread_spinlock_t *lock);

int pthread_spin_unlock(pthread_spinlock_t *lock);

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

int pthread_spin_destroy(pthread_spinlock_t *lock);

自旋锁 vs. 互斥锁

特性自旋锁(Spinlock)互斥锁(Mutex)
等待方式忙等待(CPU 空转)阻塞(线程休眠,让出 CPU)
适用场景临界区执行时间短(< CPU 切换时间)临界区执行时间长
CPU 占用高(占用 CPU 循环检查)低(线程休眠)
实现复杂度简单(通常用原子操作实现)较复杂(依赖 OS 调度)
适用环境多核 CPU单核/多核均可

读者写者问题

读者写者问题具有以下特点:

  • 一个交易场所---写者写入数据,读者读数据
  • 两种角色---读者,写者
  • 三种关系
    • 读者和读者---并发
    • 写者和写者---互斥
    • 读者和写者---互斥 && 同步

生产者消费者模型中的消费者会将数据取走,而读者不会,这也是为什么读者之间不需要互斥,而是并发执行。

读多写少会导致读端竞争锁能力强,导致写端长时间竞争不到锁导致饥饿问题

 读者优先(伪代码)

核心思想

  • 只要有一个读者正在读,后续的读者可以直接进入,而写者必须等待所有读者完成。

  • 可能导致写者饥饿(如果一直有新的读者到来,写者可能永远无法执行)。

实现方式

  • 使用:

    • read_count(记录当前读者数量)

    • mutex(保护 read_count

    • rw_mutex(控制写者互斥访问)

// 全局变量
int read_count = 0;
semaphore rw_mutex = 1;  // 读写互斥锁
semaphore mutex = 1;     // 保护 read_count// 读者
void reader() {wait(mutex);         // 保护 read_countread_count++;if (read_count == 1) // 第一个读者要锁住写者wait(rw_mutex);signal(mutex);// 执行读操作...wait(mutex);read_count--;if (read_count == 0) // 最后一个读者释放写锁signal(rw_mutex);signal(mutex);
}// 写者
void writer() {wait(rw_mutex);      // 直接尝试获取写锁// 执行写操作...signal(rw_mutex);
}

写者两状态:未加锁、加锁,当没有加锁时即没有写入,读者竞争写锁,防止写端写入,当读者全部退出时释放写锁,当写端加锁即正在写入时,第一个读者阻塞等待写端的锁

特点

✅ 读者可以并发读,提高吞吐量
❌ 可能导致写者饥饿(如果读者源源不断,写者可能永远无法执行)

 写者优先

核心思想

  • 如果有写者在等待,新读者必须等待,直到所有写者完成。

  • 避免写者饥饿,但可能导致读者吞吐量下降

实现方式

  • 增加:

    • write_count(记录等待/正在写的写者数量)

    • read_try(阻止新读者在有写者等待时进入)

// 全局变量
int read_count = 0, write_count = 0;
semaphore rw_mutex = 1;  // 读写互斥锁
semaphore mutex = 1;     // 保护 read_count/write_count
semaphore read_try = 1;  // 控制读者尝试进入// 读者
void reader() {wait(read_try);      // 尝试进入(可能被写者阻塞)wait(mutex);read_count++;if (read_count == 1)wait(rw_mutex);signal(mutex);signal(read_try);    // 允许其他读者尝试// 执行读操作...wait(mutex);read_count--;if (read_count == 0)signal(rw_mutex);signal(mutex);
}// 写者
void writer() {wait(mutex);write_count++;if (write_count == 1)wait(read_try);  // 阻止新读者进入signal(mutex);wait(rw_mutex);// 执行写操作...signal(rw_mutex);wait(mutex);write_count--;if (write_count == 0)signal(read_try); // 允许新读者进入signal(mutex);
}

特点

✅ 避免写者饥饿
❌ 读者可能被延迟(如果有写者在等待)

读写锁

在编写多线程的时候,有一种情况是十分常见的。那就是有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。

读写锁接口

设置读写优先

int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);

/*

pref 共有 3 种选择

PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况 PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和 PTHREAD_RWLOCK_PREFER_READER_NP 一致 PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁 */

初始化

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict 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);  

读者优先的读写锁(伪代码)

lock(&rlock);
read_count++;
if(read_count==1)    lock(&wlock);
unlock(&rlock);//进行读取lock(&rlock);
read_count--;
if(read_count==0)    unlock(&wlock);
unlock(&rlock);
http://www.lryc.cn/news/582001.html

相关文章:

  • WebRTC与RTMP
  • GPT5完全多模态架构拆解:实时视频生成如何颠覆内容创作
  • 什么是去中心化 AI?区块链驱动智能的初学者指南
  • 【C++指南】STL queue 完全解读(一):原理剖析与实战应用
  • 开源鸿蒙(OpenHarmony)桌面版全面解析:架构适配、设备支持与开发实战
  • Matlab自学笔记六十二:求解三角函数方程的通解周期解
  • 【JAVAFX】webview导入本地html并传入参数
  • 【论文笔记】World Models for Autonomous Driving: An Initial Survey
  • excel日志表介绍
  • C++学习笔记01(自学草稿)
  • 国民经济行业分类 GB/T 4754—2017 (PDF和exce版本)
  • 中电金信 :十问高质量数据集:金融大模型价值重塑有“据”可循
  • 【Unity笔记】Unity 粒子系统 Triggers 使用解析:监听粒子进入与离开区域并触发事件
  • maven 发布到中央仓库常用脚本-02
  • .NET9 实现 JSON 序列化和反序列化(Newtonsoft.Json System.Text.Json)性能测试
  • ArcGIS 水文分析升级:基于深度学习的流域洪水演进过程模拟
  • 3S技术+ArcGIS/ENVI全流程实战:水文、气象、灾害、生态、环境及卫生等领域应用
  • 语音交互新纪元:Hugging Face LeRobot如何让机器人真正“懂你”
  • validate CRI v1 image API for endpoint “unix:///run/containerd/containerd.sock“
  • 华为OD 2025B卷 机试 - 拼接URL (C++PythonJAVAJSC语言)
  • 用U盘启动制作centos系统最常见报错,系统卡住无法继续问题(手把手)
  • 深入解析与彻底解决 Android 集成 Flutter Boost 时页面闪烁问题
  • K8s-服务发布进阶
  • Web后端开发-分层解耦
  • 02 除了前面常见图表,还有许多更细分或专业的可视化类型,尤其是在特定领域(如金融、工程、生物信息等)。
  • Java学习第二十部分——EasyMock
  • FastAPI依赖注入:构建高可维护API的核心理念与实战
  • 【每日算法】专题六_分治 - 快速排序
  • 如何设计一个“真正可复用”的前端组件?
  • 上海新华医院奉贤院区:以元宇宙技术重构未来医疗生态