单例模式和读者写者问题

文章目录

  • [10. 线程安全的单例模式](#10. 线程安全的单例模式)
    • [10.1 什么是设计模式](#10.1 什么是设计模式)
    • [10.2 什么是单例模式](#10.2 什么是单例模式)
    • [10.3 单例模式的特点](#10.3 单例模式的特点)
    • [10.4 饿汉方式和懒汉方式](#10.4 饿汉方式和懒汉方式)
    • [10.5 单例模式的线程池](#10.5 单例模式的线程池)
  • [11. STL和智能指针的线程安全 问题](#11. STL和智能指针的线程安全 问题)
    • [11.1 STL中的容器是否是线程安全的?](#11.1 STL中的容器是否是线程安全的?)
    • [11.2 智能指针是否是线程安全的?](#11.2 智能指针是否是线程安全的?)
  • [12. 其他常见的各种锁](#12. 其他常见的各种锁)
  • [13. 读者写者问题](#13. 读者写者问题)
    • [13.1 概念](#13.1 概念)
    • [13.2 读写锁接口](#13.2 读写锁接口)
    • [13.3 读者优先的伪代码](#13.3 读者优先的伪代码)

10. 线程安全的单例模式

10.1 什么是设计模式

设计模式(Design Pattern)是软件工程中的一种最佳实践,它是在特定场景下解决特定问题的成熟模板或方案。设计模式是面向对象软件开发过程中经过验证的经验和智慧的结晶,它们提供了一种通用的、可复用的解决方案来解决在软件设计中遇到的常见问题。

10.2 什么是单例模式

单例模式(Singleton Pattern)是一种常用的软件设计模式,其核心目的是确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。这种模式在需要控制资源访问、节省系统资源、协调系统中的共享资源时非常有用。

10.3 单例模式的特点

单例模式的主要特点包括:

  1. 唯一性:确保一个类只有一个实例。
  2. 全局访问:提供一个全局访问点来获取这个唯一的实例。

10.4 饿汉方式和懒汉方式

饿汉方式(Eager Initialization)

饿汉方式是指在程序启动时就立即创建单例对象 。这种方式的优点是简单、线程安全 ,因为对象的创建是在程序启动时完成的,不存在多线程同时访问的问题。缺点是如果单例对象的创建比较耗时或者占用资源较多,可能会影响程序的启动速度

懒汉方式的单例模式实现如下:

cpp 复制代码
class Singleton 
{
public:
    static Singleton& getInstance() 
    {
        return instance;
    }
private:
    static Singleton instance; // 静态成员变量,饿汉式,直接在类中创建实例
    Singleton() {} // 私有构造函数
    Singleton(const Singleton&) = delete; // 禁止拷贝构造
    Singleton& operator=(const Singleton&) = delete; // 禁止赋值操作
};

// 在类外初始化静态成员变量
Singleton Singleton::instance;

懒汉方式(Lazy Initialization)

懒汉方式是指在第一次使用单例对象时才创建它 。这种方式的优点是可以延迟对象的创建,从而加快程序的启动速度 ,并且只有在真正需要时才创建对象。缺点是如果多个线程同时访问单例对象,可能会存在线程安全问题,所以要加锁。

线程不安全的懒汉方式实现的单例模式

cpp 复制代码
class Singleton 
{
public:
    static Singleton* getInstance() 
    {
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
private:
    static Singleton* instance; // 静态成员变量指针,懒汉式,延迟创建实例
    Singleton() {} // 私有构造函数
    Singleton(const Singleton&) = delete; // 禁止拷贝构造
    Singleton& operator=(const Singleton&) = delete; // 禁止赋值操作
};

// 在类外初始化静态成员变量指针
Singleton* Singleton::instance = nullptr;

使用局部静态变量来实现线程安全的懒汉式单例,因为局部静态变量的初始化在C++ 11中是线程安全的。

cpp 复制代码
class Singleton 
{
public:
    static Singleton& getInstance() {
        static Singleton instance; // 局部静态变量,线程安全的懒汉式
        return instance;
    }
private:
    Singleton() {} // 私有构造函数
    Singleton(const Singleton&) = delete; // 禁止拷贝构造
    Singleton& operator=(const Singleton&) = delete; // 禁止赋值操作
};

getInstance方法中的局部静态变量instance只会在第一次调用getInstance时被创建,之后的调用都会返回同一个实例,这种方式既实现了懒汉式的延迟加载,又保证了线程安全。

使用加锁的方式

cpp 复制代码
class Singleton 
{
public:
    static Singleton* getInstance() 
    {
        if (instance == nullptr) {		// 双重判定空指针, 降低锁冲突的概率, 提高性能.
            pthread_mutex_lock(&mutex);	// 使用互斥锁, 保证多线程情况下也只调用一次 new.
            if (instance == nullptr) 
            	instance = new Singleton();
           	pthread_mutex_unlock(&mutex);
        }
        return instance;
    }
private:
    static Singleton* instance; // 静态成员变量指针,懒汉式,延迟创建实例
    static pthread_mutex_t mutex;		// 锁
    Singleton() {} // 私有构造函数
    Singleton(const Singleton&) = delete; // 禁止拷贝构造
    Singleton& operator=(const Singleton&) = delete; // 禁止赋值操作
};

// 在类外初始化静态成员变量指针
Singleton* Singleton::instance = nullptr;
pthread_mutex_t Singleton::mutex = PTHREAD_MUTEX_INITIALIZER;

10.5 单例模式的线程池

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <semaphore.h>
#include <pthread.h>
#include <vector>
#include <queue>
using namespace std;
struct ThreadData
{
    pthread_t tid;
    string name;
};

// T表示任务的类型
template<class T>
class ThreadPool
{
public:
	// ...
    static ThreadPool* GetInstance()
    {   
        if(tp == nullptr) {
            pthread_mutex_lock(&lock);
            if(tp == nullptr) 
                tp = new ThreadPool<T>();
            pthread_mutex_unlock(&lock);
        }
        return tp;
    }
private:
    ThreadPool(size_t num = defaultNum) : _threads(num) 
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

    ThreadPool(const ThreadPool& tp) = delete;
    const ThreadPool operator=(const ThreadPool& tp) = delete;

    vector<ThreadData> _threads;
    queue<T> _tasks;    // 任务,这是临界资源
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;

    static ThreadPool* 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;

11. STL和智能指针的线程安全 问题

11.1 STL中的容器是否是线程安全的?

原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.

11.2 智能指针是否是线程安全的?

对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.

对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.

12. 其他常见的各种锁

  • 悲观锁(Pessimistic Locking):在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
  • 乐观锁(Optimistic Locking):每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
    • CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
  • 自旋锁(Spinlock):当一个线程尝试获取一个已经被其他线程持有的锁时,该线程不会立即进入等待状态(即不会释放CPU),而是在原地"自旋",也就是不停地进行忙等待(busy-waiting),直到获取到锁。
    • 当一个线程尝试获取一个已经被占用的自旋锁时,它会在原地循环检查锁的状态,直到锁变为可用。
    • 自旋锁不会使线程进入睡眠状态,因此它是一种非阻塞的同步机制。
    • 由于线程不会进入睡眠状态,自旋锁避免了线程上下文切换的开销。
    • 由于自旋锁会导致CPU资源的占用,因此它更适合于那些预计会很快释放的锁。如果锁的持有时间较长,自旋锁可能会导致CPU资源的浪费。
    • 如果持有自旋锁的线程发生阻塞,那么等待该锁的线程可能会无限期地自旋下去,导致死锁。
    • 之前使用的都属于悲观锁,是否采用自旋锁取决于线程在临界资源会待多长时间。
  • 公平锁(Fair Lock)是一种锁机制,它确保了线程获取锁的顺序与它们请求锁的顺序相同。换句话说,公平锁保证了"先来先服务"(FIFO,First-In-First-Out)的原则,即最先请求锁的线程将最先获得该锁。
  • 非公平锁(Non-Fair Lock)是一种锁机制,它不保证线程获取锁的顺序与它们请求锁的顺序相同。这意味着当一个线程尝试获取一个非公平锁时,它可能会与已经等待该锁的其他线程竞争,而不管这些线程等待了多久。

13. 读者写者问题

13.1 概念

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

  • 3种关系:
    • 写者 vs 写者 (互斥)
    • 读者 vs 写者 (互斥,同步)
    • 读者 vs 读者 (共享关系)这是和生产消费者模型的区别
  • 2个角色:读者和写者
  • 1个交易场所:数据交换的地点

为什么读者写者问题中读者和读者关系是共享 而生产消费者模型中 消费者和消费者的关系是互斥呢?

因为读者并不会对数据做处理,只是对数据进行读操作。而消费者会对数据进行数据处理。


一般来说,读者多,写者少。所以概率上讲读者更容易竞争到锁,写者可能会出现饥饿问题。

这是读者写者问题的特点。也可以更改这个现象,设置同步策略,让写者优先

  • 读者优先:在这种策略下,如果读者和写者同时等待访问临界区,读者会被优先允许进入。这种策略可以减少写者的等待时间,因为读者通常持有锁的时间较短。然而,如果读者持续不断地访问数据,写者可能会遭遇饥饿,即长时间无法获得对数据的访问权。
  • 写者优先::在这种策略下,如果读者和写者同时等待访问临界区,写者会被优先允许进入。这种策略可以防止读者饥饿,因为写者一旦获得访问权,会阻止新的读者进入,直到写者完成写操作。但是,如果写者频繁地访问数据,读者可能会遭遇饥饿。

13.2 读写锁接口

c 复制代码
// 初始化
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t* restrict attr);
c 复制代码
// 销毁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
c 复制代码
// 加锁和解锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

13.3 读者优先的伪代码

c 复制代码
int reader_count = 0;
mutex_t rlock, wlock;

// 读者加锁 && 解锁
lock(&rlock);
read_count++;
if(reader_count==1)	lock(&wlock);
unlock(&rlock);
// 读者进行读取
lock(&rlock);
reader_count--;
if(reader_count==0)	unlock(&wlock);
unlock(rlock);

// 写者加锁 && 解锁
lock(&wlock);
// 写者进行写入操作
unlock(&wlock)
  1. 读者加锁
    • 首先,读者尝试获取 rlock 锁,以安全地修改 reader_count 变量。
    • 获取 rlock 后,读者增加 reader_count 的值。
    • 如果这是第一个进入的读者(即 reader_count 从0变为1),则需要获取 wlock 锁,以阻止写者写入数据。这是因为一旦有读者在读取数据,写者就不应该修改数据,否则会影响读者读取的一致性。(读者优先!)
    • 完成 reader_count 的增加和可能的 wlock 获取后,读者释放 rlock 锁。
  2. 读者解锁
    • 读者完成读取操作后,再次获取 rlock 锁,以便安全地减少 reader_count 的值。
    • 如果这是最后一个离开的读者(即 reader_count 从1变为0),则需要释放 wlock 锁,允许写者进行写入操作。
    • 完成 reader_count 的减少和可能的 wlock 释放后,读者释放 rlock 锁。
  3. 写者加锁
    • 写者尝试获取 wlock 锁,以独占访问权进行写入操作。
    • 一旦获取 wlock 锁,写者可以安全地进行写入操作,因为此时没有读者在读取数据。
  4. 写者解锁
    • 写者完成写入操作后,释放 wlock 锁,允许其他读者或写者访问数据。
相关推荐
Brookty2 分钟前
【MySQL】JDBC编程
java·数据库·后端·学习·mysql·jdbc
能工智人小辰16 分钟前
二刷 苍穹外卖day10(含bug修改)
java·开发语言
DKPT17 分钟前
Java设计模式之结构型模式(外观模式)介绍与说明
java·开发语言·笔记·学习·设计模式
缘来是庄19 分钟前
设计模式之外观模式
java·设计模式·外观模式
LL.。41 分钟前
同步回调和异步回调
开发语言·前端·javascript
0wioiw01 小时前
Python基础(吃洋葱小游戏)
开发语言·python·pygame
知其然亦知其所以然1 小时前
JVM社招面试题:队列和栈是什么?有什么区别?我在面试现场讲了个故事…
java·后端·面试
栗子~~1 小时前
Python实战- Milvus 向量库 使用相关方法demo
开发语言·python·milvus
狐凄1 小时前
Python实例题:基于 Flask 的在线聊天系统
开发语言·python
狐凄1 小时前
Python实例题:基于 Flask 的任务管理系统
开发语言·python