目录
线程安全的单例模式
什么是单例模式
单例模式的特点
饿汉实现方式和懒汉实现方式
饿汉⽅式实现单例模式
懒汉⽅式实现单例模式
懒汉⽅式实现单例模式(线程安全版本)
单例式线程池
ThreadPool.hpp
threadpool.cc
运行结果
线程安全和重⼊问题
常⻅锁概念
死锁
死锁四个必要条件
避免死锁
破坏死锁的四个必要条件
避免死锁算法
STL中的容器是否是线程安全的?
智能指针是否是线程安全的?
其他常⻅的各种锁
线程安全的单例模式
什么是单例模式
单例模式的特点
某些类, 只应该具有⼀个对象(实例), 就称之为单例.
例如⼀个男⼈只能有⼀个媳妇. 在很多服务器开发场景中, 经常需要让服务器加载很多的数据(上百G) 到内存中. 此时往往要⽤⼀个单例的类来管理这些数据.
单例模式 分为饿汉模式和懒汉模式
饿汉实现方式和懒汉实现方式
[洗碗的例⼦]
吃完饭, ⽴刻洗碗, 这种就是饿汉⽅式. 因为下⼀顿吃的时候可以⽴刻拿着碗就能吃饭.
吃完饭, 先把碗放下, 然后下⼀顿饭⽤到这个碗了再洗碗, 就是懒汉⽅式.
饿汉模式: 直接准备好
懒汉模式: 用时才准备,延时加载 例如: 写时拷贝 ,new malloc 中真实物理空间的加载
懒汉方式最核心的思想是 "延时加载". 从而能够优化服务器的启动速度.
饿汉⽅式实现单例模式
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 对象的实例.但是后续再次调⽤,就没有问题了.
懒汉⽅式实现单例模式(线程安全版本)
// 懒汉模式, 线程安全
template <typename T>
class Singleton
{volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化. static std::mutex lock;
public:static T* GetInstance() {if (inst == NULL) { // 双重判定空指针, 降低锁冲突的概率, 提⾼性能. lock.lock(); // 使⽤互斥锁, 保证多线程情况下也只调⽤⼀次 new. if (inst == NULL) {inst = new T();}lock.unlock();}return inst;}
};
注意事项:
1. 加锁解锁的位置
2. 双重 if 判定,避免不必要的锁竞争
3. volatile关键字防⽌过度优化
单例式线程池
ThreadPool.hpp
#pragma once
#include <queue>
#include <iostream>
#include <string>
#include <vector> //用vector管理线程
#include <memory>
#include "log.hpp"
#include "Thread.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"namespace ThreadPoolModule
{using namespace LockModule;using namespace ThreadModule;using namespace CondModule;using namespace LogModule;const static int defaultnum = 5;using thread_t = std::shared_ptr<Thread>;void Defaulttest(){while (true){LOG(LogLevel::INFO) << "test";sleep(1);}}template <typename T>class ThreadPool{private:bool IsEmpty(){return _taskq.empty();}// 执行任务void HandlerTask(std::string name){LOG(LogLevel::INFO)<<"线程"<< name <<"进入handlerTask";// 线程醒来就一直执行while (true){T t;{LockGuard lockguard(_mutex);while (IsEmpty() && _isrunning){_wait_num++;_cond.Wait(_mutex);_wait_num--;}// 任务队列为空 && 线程池退出了才退出 ,重要重要 if(IsEmpty() && !_isrunning)break;// 1.拿任务t = _taskq.front();_taskq.pop();}//在临界区外 ,处理任务 ,效率更高// 2.处理任务 规定传入的所有的任务, 必须提供()方法t(name);}LOG(LogLevel::INFO) << "线程: " << name << " 退出";}ThreadPool(const ThreadPool<T> &) = delete;//拷贝构造禁掉ThreadPool<T>& operator=(const ThreadPool<T> &) = delete;//赋值重载禁掉ThreadPool(int num = defaultnum) //构造函数设为私有: _num(num),_wait_num(0),_isrunning(false){// 创建num个线程for (int i = 0; i < num; i++){//bind 此时所有创建出来的线程,转而去执行HandlerTask_threads.push_back(std::make_shared<Thread>(std::bind(&ThreadPool::HandlerTask, this,std::placeholders::_1 )));LOG(LogLevel::INFO) << "构建线程" << _threads.back()->Name() << "成功";}}public:static ThreadPool<T> * getInstance() //单例模式{if(_instance == NULL){LockGuard lockguard(_mutex_singleton);if(_instance == NULL){LOG(LogLevel::INFO) << "单例首次被执行,需要加载对象...";_instance = new ThreadPool<T>();}}return _instance;}~ThreadPool(){}void Start(){if(_isrunning) return;_isrunning = true; // bug fix??for (auto &thread_ptr : _threads){thread_ptr->Start();LOG(LogLevel::INFO) << "启动线程" << thread_ptr->Name() << "成功";}}void Wait(){for (auto &thread_ptr : _threads){thread_ptr->Join();LOG(LogLevel::INFO) << "回收线程" << thread_ptr->Name() << "成功";}}//任务入队列void Enqueue(T &&in)//这个会被多线程调用 ,先加锁{//只要队列扛得住 ,就一直加LockGuard lockguard(_mutex);if(!_isrunning) return;_taskq.push(std::move(in));if(_wait_num > 0 ) _cond.Notify();}//退出线程池void Stop(){LockGuard lockguard(_mutex);if(_isrunning){// 3. 不能在入任务了_isrunning = false; // 不工作// 1. 让线程自己退出(要唤醒) && // 2. 历史的任务被处理完了if(_wait_num>0)_cond.NotifyAll();}}private:int _num; // 线程个数std::queue<T> _taskq; // 任务队列 是临界资源std::vector<thread_t> _threads; // 管理线程 ,其中是线程的指针Mutex _mutex;Cond _cond;int _wait_num;bool _isrunning ; //线程池的运行状态static ThreadPool<T>* _instance; //单例模式的静态指针static Mutex _mutex_singleton;//只用来保护单例};//静态方法初始化应放在类外template<typename T>ThreadPool<T> *ThreadPool<T>::_instance = NULL;template<typename T>Mutex ThreadPool<T>::_mutex_singleton; //只用来保护单例
}
threadpool.cc
#include"ThreadPool.hpp"
#include<memory>
#include"Task.hpp"
using namespace ThreadPoolModule;int main()
{ENABLE_CONSOLE_LOG();ThreadPool<task_t>::getInstance()->Start();char c;int cnt = 5;while (cnt){// std::cin >> c;ThreadPool<task_t>::getInstance()->Enqueue(Push);cnt--;sleep(1);}ThreadPool<task_t>::getInstance()->Stop();ThreadPool<task_t>::getInstance()->Wait();
}
运行结果
线程安全和重⼊问题
线程安全描述的是线程
重入描述的是函数
线程安全:
就是多个线程在访问共享资源时,能够正确地执⾏,不会相互⼲扰或破坏彼此的执⾏结果。⼀般⽽⾔,多个线程并发同⼀段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或者静态变量进⾏操作,并且没有锁保护的情况下,容易出现该问题。
重⼊:
同⼀个函数被不同的执⾏流调⽤,当前⼀个流程还没有执⾏完,就有其他的执⾏流再次进⼊, 我们称之为重⼊。⼀个函数在重⼊的情况下,运⾏结果不会出现任何不同或者任何问题,则该函数被 称为可重⼊函数,否则,是不可重⼊函数。
重⼊其实可以分为两种情况
- 多线程重⼊函数
- 信号导致⼀个执⾏流重复进⼊函数
常⻅锁概念
死锁
- 死锁是指在⼀组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源⽽处于的⼀种永久等待状态。
- 为了方便表述,假设现在线程A,线程B必须同时持有锁1和锁2,才能进⾏后续资源的访问
申请⼀把锁是原⼦的,但是申请两把锁就不⼀定了
造成的结果是
死锁四个必要条件
- 互斥条件:⼀个资源每次只能被⼀个执⾏流使用
- 请求与保持条件:⼀个执⾏流因请求资源⽽阻塞时,对已获得的资源保持不放
- 不剥夺条件:⼀个执⾏流已获得的资源,在末使⽤完之前,不能强⾏剥夺
- 循环等待条件:若⼲执⾏流之间形成⼀种头尾相接的循环等待资源的关系
避免死锁
破坏死锁的四个必要条件
破坏循环等待条件问题:资源⼀次性分配,使⽤超时机制、加锁顺序⼀致
// 下⾯的C++不写了,理解就可以
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <unistd.h>
// 定义两个共享资源(整数变量)和两个互斥锁
int shared_resource1 = 0;
int shared_resource2 = 0;
std::mutex mtx1, mtx2;// ⼀个函数,同时访问两个共享资源
void access_shared_resources()
{// std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);// std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);// // 使⽤ std::lock 同时锁定两个互斥锁 // std::lock(lock1, lock2);// 现在两个互斥锁都已锁定,可以安全地访问共享资源 int cnt = 10000;while (cnt){++shared_resource1;++shared_resource2;cnt--;}// 当离开 access_shared_resources 的作⽤域时,lock1 和 lock2 的析构函数会被⾃动调⽤ // 这会导致它们各⾃的互斥量被⾃动解锁
}// 模拟多线程同时访问共享资源的场景
void simulate_concurrent_access()
{std::vector<std::thread> threads;// 创建多个线程来模拟并发访问 for (int i = 0; i < 10; ++i){threads.emplace_back(access_shared_resources);}// 等待所有线程完成 for (auto &thread : threads){thread.join();}// 输出共享资源的最终状态 std::cout << "Shared Resource 1: " << shared_resource1 << std::endl;std::cout << "Shared Resource 2: " << shared_resource2 << std::endl;
}int main()
{simulate_concurrent_access();return 0;
}
$ ./a.out // 不⼀次申请
Shared Resource 1: 94416
Shared Resource 2: 94536$ ./a.out // ⼀次申请
Shared Resource 1: 100000
Shared Resource 2: 100000
避免死锁算法
- 死锁检测算法
- 银⾏家算法
STL中的容器是否是线程安全的?
不安全.
原因是, STL 的设计初衷是将性能挖掘到极致, ⽽⼀旦涉及到加锁保证线程安全, 会对性能造成巨⼤的影响. ⽽且对于不同的容器, 加锁⽅式的不同, 性能可能也不同(例如hash表的锁表和锁桶). 因此 STL 默认不是线程安全.
如果需要在多线程环境下使⽤, 往往需要调⽤者⾃⾏保证线程安全.
智能指针是否是线程安全的?
智能指针是安全的,指针指向的对象不一定.
对于 unique_ptr, 由于只是在当前代码块范围内⽣效, 因此不涉及线程安全问题.
对于 shared_ptr, 多个对象需要共⽤⼀个引⽤计数变量, 所以会存在线程安全问题. 但是标准库实现的时 候考虑到了这个问题, 基于原⼦操作(CAS)的⽅式保证 shared_ptr 能够⾼效, 原⼦的操作引⽤计数.
其他常⻅的各种锁
- 悲观锁:在每次取数据时,总是担⼼数据会被其他线程修改,所以会在取数据前先加锁(读锁, 写锁,⾏锁等),当其他线程想要访问数据时,被阻塞挂起。
- 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新 数据前,会判断其他数据在更新前有没有对数据进⾏修改。主要采⽤两种⽅式:版本号机制和 CAS操作。
- CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则⽤新值更 新。若不等则失败,失败则重试,⼀般是⼀个⾃旋的过程,即不断重试。
- ⾃旋锁,读写锁.