文章目录
- 10. 线程安全的单例模式
- 10.1 什么是设计模式
- 10.2 什么是单例模式
- 10.3 单例模式的特点
- 10.4 饿汉方式和懒汉方式
- 10.5 单例模式的线程池
- 11. STL和智能指针的线程安全 问题
- 11.1 STL中的容器是否是线程安全的?
- 11.2 智能指针是否是线程安全的?
- 12. 其他常见的各种锁
- 13. 读者写者问题
- 13.1 概念
- 13.2 读写锁接口
- 13.3 读者优先的伪代码
10. 线程安全的单例模式
10.1 什么是设计模式
设计模式(Design Pattern)是软件工程中的一种最佳实践,它是在特定场景下解决特定问题的成熟模板或方案。设计模式是面向对象软件开发过程中经过验证的经验和智慧的结晶,它们提供了一种通用的、可复用的解决方案来解决在软件设计中遇到的常见问题。
10.2 什么是单例模式
单例模式(Singleton Pattern)是一种常用的软件设计模式,其核心目的是确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。这种模式在需要控制资源访问、节省系统资源、协调系统中的共享资源时非常有用。
10.3 单例模式的特点
单例模式的主要特点包括:
- 唯一性:确保一个类只有一个实例。
- 全局访问:提供一个全局访问点来获取这个唯一的实例。
10.4 饿汉方式和懒汉方式
饿汉方式(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;
10.5 单例模式的线程池
#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 读写锁接口
// 初始化
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);
13.3 读者优先的伪代码
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
锁,允许其他读者或写者访问数据。
- 写者完成写入操作后,释放