目录一 线程池的实现1 设计线程池2 线程安全的单例模式1饿汉实现方式和懒汉实现方式2饿汉方式实现单例模式3懒汉方式实现单例模式二 线程安全和重入问题1 概念2 结论三 常见锁概念1 死锁2 死锁的四个必要条件一 线程池的实现1 设计线程池main.cc#include ThreadPool.hpp #include memory int main() { // 开启日志输出到控制台写的策略宏 ENABLE_CONSOLE_LOG_STRATEGY(); 创建线程池对象智能指针自动释放 std::unique_ptrThreadPool tp std::make_uniqueThreadPool(); tp-Start(); // 循环往线程池丢任务 while (true) { tp-Enqueue(task); } tp-Stop(); tp-Wait(); return 0; }ThreadPool.hpp#pragma once #include iostream #include vector #include Thread.hpp #include Logger.hpp static const int gnum 5; using namespace LogModule; void DefaultRun() { char name[64]; pthread_getname_np(pthread_self(), name, sizeof(name)); while(true) { LOG(LogLevel::DEBUG) name 线程执行默认方法; sleep(1); } } class ThreadPool { public: // 构造函数默认创建5个线程 ThreadPool(int num gnum):_num(num) { // 循环创建线程放入vector for(int i 0; i _num; i) { // 把 DefaultRun 函数传给线程作为执行函数 _threads.emplace_back(DefaultRun); } } void Start() { } void Enqueue() { } void Stop() { } void Wait() { } ~ThreadPool(){} private: std::vectorThread _threads; // 所有线程 int _num; };创建线程对象不代表线程被调用线程默认执行函数 DefaultRun ()---作用每个子线程启动后都会循环打印自己的线程名 日志我们增加一个bool _isrunning表示线程是否运行我们在线程stop中增加判断线程是否正在运行只有在运行的线程才能stop线程池是一个典型的生产者消费者模型同时我们也要支持用户是单生产者或者多生产者的情况。那么任务队列就是生产与消费的临界资源我们就会选择互斥与同步----互斥锁保护临界资源同时也需要使用条件变量有任务就处理任务没有任务就让线程去等待使用std::bind把一个类内方法和一个对象绑定我们进入临界区时要先判断任务队列是否为空为空的话线程就要到条件变量的队列中等待while(IsTaskQueueEmpty()) { _cond.Wait(_lock); }我们想唤醒线程时需要设置唤醒条件维护一个计数器让我们知道有多少线程在休眠线程总数-休眠数正在工作的线程数我们创建一个任务文件 Task.hpp#pragma once #include iostream #include functional #include pthread.h #include Logger.hpp using task_t std::functionvoid(); using namespace LogModule; // 1. 全局函数 void task1() { char name[64]; pthread_getname_np(pthread_self(), name, sizeof(name)); LOG(LogLevel::DEBUG) 执行任务1: 打印消息 | name |; } void task2() { char name[64]; pthread_getname_np(pthread_self(), name, sizeof(name)); LOG(LogLevel::DEBUG) 执行任务2: 计算 11 1 1 | name |; }所以我们来写线程池的任务循环逻辑线程退出时1 不能把正在处理任务的线程给取消----pthread_cancel不能使用2 我们要让线程正常结束处理完剩余任务才能退出线程要退出就不能再Push所以线程退出的条件是线程自己退出任务队列为空----两个都满足时线程才能退出线程是否退出的判断一共有三种1任务队列不为空2任务队列不为空线程池要退出3任务队列为空线程池要退出只有最后一种情况是线程真正会退出的情况template typename T class ThreadPool { private: bool IsTaskQueueEmpty() { return _queue.empty(); } T PopHelper() { T t _queue.front(); _queue.pop(); return t; } void ThreadRoutine() { char name[64]; pthread_getname_np(pthread_self(), name, sizeof(name)); while (true) { T task; { // 临界区 LockGuard lockguard(_lock); // 1. 没有任务 线程池不退出 - 允许休眠 while (IsTaskQueueEmpty() _isrunning) { _sleeper_cnt; LOG(LogLevel::DEBUG) 没有任务, 线程休眠: | name |; _cond.Wait(_lock); LOG(LogLevel::DEBUG) 有任务, 线程唤醒: | name |; _sleeper_cnt--; } // 2. 没有任务 线程池退出 -- 线程结束 if (IsTaskQueueEmpty() !_isrunning) { LOG(LogLevel::INFO) Thread: name quit; break; } // 3. 有任务 线程池退出 , 不关心线程有没有退出 // 3. 有任务 线程池没退出 , 不关心线程有没有退出 // 目前: 一定是有任务的 task PopHelper(); } task(); // 处理任务不应该在临界区内部处理获取任务之后任务已经从公有变成私有了 } }2 线程安全的单例模式单例模式在程序内部只允许存在一个对象。就是一个对象在当前进程运行周期内只能存在一份注意区分和程序启动一次是不一样的程序启动一次是系统控制的在很多服务器开发场景中服务器经常需要加载大量数据上百 GB到内存中。此时通常会使用一个单例类来管理这些数据。单例模式的实现方式有两种饿汉实现和懒汉实现1饿汉实现方式和懒汉实现方式我们用一个例子来理解一下吃完饭后立刻洗碗下一顿用碗时可以马上拿到干净的碗这就像饿汉方式。吃完饭后先不洗碗等到下一顿需要用碗时再去洗这就像懒汉方式2饿汉方式实现单例模式饿汉方式它的特点是程序一启动就创建唯一实例无论是否马上使用。特点立即创建实例单例对象在类加载时就生成不需要等到第一次访问。线程安全由于实例在程序启动时就创建天然线程安全无需加锁。访问方便随时可以拿到实例使用时无需等待如果创建的对象特别大例如5个G就会导致创建进程的速度变慢也就是启动程序的速度变慢饿汉是创建一个静态实例class Singleton { private: Singleton() {} // 构造函数私有 static Singleton instance; // 静态实例程序启动时创建 public: static Singleton GetInstance() { return instance; // 直接返回已创建实例 } }; // 静态成员初始化 Singleton Singleton::instance;3懒汉方式实现单例模式懒汉方式它的特点是在第一次使用时才创建实例而不是程序启动就创建。特点延迟创建只有在第一次调用GetInstance()时单例对象才会被创建。节省资源如果程序运行过程中可能不需要该对象就不会浪费内存。线程安全需注意在多线程环境下需要加锁或使用 C11 的std::call_once来保证线程安全和饿汉不一样的是懒汉是创建指针而不是创建对象在要使用对象的时候才会创建class Singleton { private: Singleton() {} // 构造函数私有 static Singleton* instance; // 静态指针初始为 nullptr public: static Singleton* GetInstance() { if (instance nullptr) { // 第一次访问时才创建 instance new Singleton(); } return instance; } }; // 静态成员初始化 Singleton* Singleton::instance nullptr;我们把我们的线程池改成懒汉方式实现首先要改成单例模式要把线程池本身改成单例----1构造函数私有化禁止外部直接创建对象。2删除拷贝构造和赋值运算符防止复制。3提供静态方法GetInstance()获取唯一实static ThreadPoolT* _instance;注意静态成员的初始化是在类外提供获取单例的函数ThreadPoolT* Getintance()如果instancenullptr说明当前单例没有创建对象instance是静态成员只属于类不属于对象但是成员函数必须以对象的方式调用所以这个函数必须设置成静态函数问题创建单例的过程可不可能多线程并发调用instance且都是第一次调用所以创建单例的过程不是线程安全的单例模式需要加锁保证单例的线程安全---static Mutex_singleton_lock;获取单例是一个频繁的动作但是创建对象这个动作只会做一次但是如果每次使用都加锁解锁效率就会大大降低了所以再加一个if判断用双if的方式实现线程安全的单例模式// 获取单例不是线程安全的 -- 解决了 // 双if判断线程安全版的单例模式 static ThreadPoolT *GetInstance() { if (_instance nullptr) { LockGuard lockguard(_singleton_lock); if (_instance nullptr)// { LOG(LogLevel::DEBUG) 首次使用创建对象; _instance new ThreadPoolT(); // 只会做一次 } } return _instance; }至此我们就完成了单例的线程池代码完整代码如下#pragma once #include iostream #include vector #include queue #include Thread.hpp #include Logger.hpp #include Mutex.hpp #include Cond.hpp static const int gnum 5; using namespace LogModule; #if 0 void DefaultRun() { char name[64]; pthread_getname_np(pthread_self(), name, sizeof(name)); while(true) { LOG(LogLevel::DEBUG) name 线程执行默认方法; sleep(1); } } #endif template typename T class ThreadPool { private: bool IsTaskQueueEmpty() { return _queue.empty(); } T PopHelper() { T t _queue.front(); _queue.pop(); return t; } void ThreadRoutine() { char name[64]; pthread_getname_np(pthread_self(), name, sizeof(name)); while (true) { T task; { // 临界区 LockGuard lockguard(_lock); // 1. 没有任务 线程池不退出 - 允许休眠 while (IsTaskQueueEmpty() _isrunning) { _sleeper_cnt; LOG(LogLevel::DEBUG) 没有任务, 线程休眠: | name |; _cond.Wait(_lock); LOG(LogLevel::DEBUG) 有任务, 线程唤醒: | name |; _sleeper_cnt--; } // 2. 没有任务 线程池退出 -- 线程结束 if (IsTaskQueueEmpty() !_isrunning) { LOG(LogLevel::INFO) Thread: name quit; break; } // 3. 有任务 线程池退出 , 不关心线程有没有退出 // 3. 有任务 线程池没退出 , 不关心线程有没有退出 // 目前: 一定是有任务的 task PopHelper(); } task(); // 处理任务不应该在临界区内部处理获取任务之后任务已经从公有变成私有了 } } private: // 单例逻辑 ThreadPool(int num gnum) : _num(num), _isrunning(false), _sleeper_cnt(0) { for (int i 0; i _num; i) { _threads.emplace_back([this]() { this-ThreadRoutine(); }); } } ThreadPool(const ThreadPoolT ) delete; ThreadPoolT operator(const ThreadPoolT ) delete; public: // 获取单例不是线程安全的 -- 解决了 // 双if判断线程安全版的单例模式 static ThreadPoolT *GetInstance() { if (_instance nullptr) { LockGuard lockguard(_singleton_lock); if (_instance nullptr) { LOG(LogLevel::DEBUG) 首次使用创建对象; _instance new ThreadPoolT(); // 只会做一次 } } return _instance; } void Start() { LockGuard lockguard(_lock); if (_isrunning) return; _isrunning true; for (auto thread : _threads) thread.start(); } void Enqueue(const T task) { LockGuard lockguard(_lock); if (!_isrunning) // 当线程池没有运行禁止push任务 return; _queue.push(task); if (_sleeper_cnt 0) _cond.NotifyOne(); } void Stop() { LockGuard lockguard(_lock); if (_isrunning) { LOG(LogLevel::DEBUG) 关闭线程池; _isrunning false; if (_sleeper_cnt 0) _cond.NotifyAll(); // for (auto thread : _threads) // thread.stop(); } } void Wait() { for (auto thread : _threads) thread.join(); } ~ThreadPool() {} private: std::vectorThread _threads; // 所有线程 int _num; bool _isrunning; // int _status; // running, stop, quit int _sleeper_cnt; std::queueT _queue; Mutex _lock; Cond _cond; // 单例模式 static ThreadPoolT *_instance; static Mutex _singleton_lock; // 保证单例线程安全的锁 }; template typename T ThreadPoolT *ThreadPoolT::_instance nullptr; template typename T Mutex ThreadPoolT::_singleton_lock;二 线程安全和重入问题1 概念线程安全多个线程访问共享资源时能够正确执行不会相互干扰或破坏彼此的执行结果。一般而言多个线程并发执行仅包含局部变量的代码时不会产生执行结果不一致的问题但如果对全局变量或静态变量进行操作且未加锁保护就极易出现线程安全问题。重入同一个函数被不同的执行流调用当前一个流程还未执行完毕就有其他执行流再次进入该函数这种情况称为重入。一个函数在重入场景下运行结果不会出现任何异常或问题则该函数被称为可重入函数反之则为不可重入函数。学到目前阶段我们可以明确重入主要分为两种情况・多线程重入函数・信号导致一个执行流重复进入函数线程安全 vs 函数重入◎ 线程安全描述线程运行时的运行特点◎ 函数重入描述的是函数的特点线程安全和函数重入可能会出现交集因为线程会调用函数2 结论可重入与线程安全的联系・函数是可重入的那就是线程安全的其实知道这一句话就够了・函数是不可重入的那就不能由多个线程使用有可能引发线程安全问题・如果一个函数中有全局变量那么这个函数既不是线程安全也不是可重入的但是这句话反过来如果线程调用函数中是线程安全的那么函数一定是可重入的吗不一定导致函数被重复调用有两种情况1多线程重复进入 2信号处理导致重复进入某个函数。在第二种极端情况下会导致函数不可被重入有些函数不是线程安全的。Eg抢票通过加锁让抢票逻辑变成安全的变成安全的含义就是这个函数可以被多线程进入了---函数就是可重入的。可重入与线程安全的联系函数是可重入的那就是线程安全的其实知道这一句话就够了函数是不可重入的那就不能由多个线程使用有可能引发线程安全问题如果一个函数中有全局变量那么这个函数既不是线程安全也不是可重入的三 常见锁概念1 死锁死锁是指在一组进程中的各个进程均占有不会释放的资源但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。我们由一个故事来理解一下死锁两个小朋友去买糖每个小朋友都只有五毛钱但是一颗糖要1块钱两个人都给对方说你把你的五毛钱给我但是谁都不答应。自己的五毛钱资源不释放且还要占用别人的资源两个人没人释放锁了假设现在线程A线程B必须同时持有锁1和锁2才能进行后续资源的访问申请一把锁是原子的但是申请两把就不一定了造成的结果是2 死锁的四个必要条件互斥条件一个资源每次只能被一个执行流使用请求与保持条件一个执行流因请求资源而阻塞时对已获得的资源保持不放不剥夺条件一个执行流已获得的资源在未使用完之前不能强行剥夺循环等待条件若干执行流之间形成头尾相接、循环等待资源的关系避免死锁就是破环四个条件中的其中一个