设计模式(Design Pattern)是软件工程中的一种最佳实践,它是在特定场景下解决特定问题的成熟模板或方案。设计模式是面向对象软件开发过程中经过验证的经验和智慧的结晶,它们提供了一种通用的、可复用的解决方案来解决在软件设计中遇到的常见问题。
单例模式(Singleton Pattern)是一种常用的软件设计模式,其核心目的是确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。这种模式在需要控制资源访问、节省系统资源、协调系统中的共享资源时非常有用。
单例模式的主要特点包括:
饿汉方式(Eager Initialization)
饿汉方式是指在程序启动时就立即创建单例对象。这种方式的优点是简单、线程安全,因为对象的创建是在程序启动时完成的,不存在多线程同时访问的问题。缺点是如果单例对象的创建比较耗时或者占用资源较多,可能会影响程序的启动速度。
懒汉方式的单例模式实现如下:
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)
懒汉方式是指在第一次使用单例对象时才创建它。这种方式的优点是可以延迟对象的创建,从而加快程序的启动速度,并且只有在真正需要时才创建对象。缺点是如果多个线程同时访问单例对象,可能会存在线程安全问题,所以要加锁。
线程不安全的懒汉方式实现的单例模式
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中是线程安全的。
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
时被创建,之后的调用都会返回同一个实例,这种方式既实现了懒汉式的延迟加载,又保证了线程安全。
使用加锁的方式
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;
#pragma once
#include
#include
#include
#include
#include
#include
#include
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;
原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.
对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.
对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.
- 悲观锁(Pessimistic Locking):在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
- 乐观锁(Optimistic Locking):每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
- CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
- 自旋锁(Spinlock):当一个线程尝试获取一个已经被其他线程持有的锁时,该线程不会立即进入等待状态(即不会释放CPU),而是在原地“自旋”,也就是不停地进行忙等待(busy-waiting),直到获取到锁。
- 当一个线程尝试获取一个已经被占用的自旋锁时,它会在原地循环检查锁的状态,直到锁变为可用。
- 自旋锁不会使线程进入睡眠状态,因此它是一种非阻塞的同步机制。
- 由于线程不会进入睡眠状态,自旋锁避免了线程上下文切换的开销。
- 由于自旋锁会导致CPU资源的占用,因此它更适合于那些预计会很快释放的锁。如果锁的持有时间较长,自旋锁可能会导致CPU资源的浪费。
- 如果持有自旋锁的线程发生阻塞,那么等待该锁的线程可能会无限期地自旋下去,导致死锁。
- 之前使用的都属于悲观锁,是否采用自旋锁取决于线程在临界资源会待多长时间。
- 公平锁(Fair Lock)是一种锁机制,它确保了线程获取锁的顺序与它们请求锁的顺序相同。换句话说,公平锁保证了“先来先服务”(FIFO,First-In-First-Out)的原则,即最先请求锁的线程将最先获得该锁。
- 非公平锁(Non-Fair Lock)是一种锁机制,它不保证线程获取锁的顺序与它们请求锁的顺序相同。这意味着当一个线程尝试获取一个非公平锁时,它可能会与已经等待该锁的其他线程竞争,而不管这些线程等待了多久。
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
- 3种关系:
- 写者 vs 写者 (互斥)
- 读者 vs 写者 (互斥,同步)
- 读者 vs 读者 (共享关系)这是和生产消费者模型的区别
- 2个角色:读者和写者
- 1个交易场所:数据交换的地点
为什么读者写者问题中读者和读者关系是共享 而生产消费者模型中 消费者和消费者的关系是互斥呢?
因为读者并不会对数据做处理,只是对数据进行读操作。而消费者会对数据进行数据处理。
一般来说,读者多,写者少。所以概率上讲读者更容易竞争到锁,写者可能会出现饥饿问题。
这是读者写者问题的特点。也可以更改这个现象,设置同步策略,让写者优先
// 初始化
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);
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)
rlock
锁,以安全地修改 reader_count
变量。rlock
后,读者增加 reader_count
的值。reader_count
从0变为1),则需要获取 wlock
锁,以阻止写者写入数据。这是因为一旦有读者在读取数据,写者就不应该修改数据,否则会影响读者读取的一致性。(读者优先!)reader_count
的增加和可能的 wlock
获取后,读者释放 rlock
锁。rlock
锁,以便安全地减少 reader_count
的值。reader_count
从1变为0),则需要释放 wlock
锁,允许写者进行写入操作。reader_count
的减少和可能的 wlock
释放后,读者释放 rlock
锁。wlock
锁,以独占访问权进行写入操作。wlock
锁,写者可以安全地进行写入操作,因为此时没有读者在读取数据。wlock
锁,允许其他读者或写者访问数据。