苏州市网站建设公司,佛山网络推广培训,资讯门户网站怎么做,wordpress如何添加备案号文章目录 前言一、v3版本二、单例模式概念特点简单实现 三、其余问题STL线程安全问题智能指针线程安全问题其他锁的概念 总结 前言 加油#xff01;#xff01;#xff01; 一、v3版本 「优化版」#xff1a;从任务队列入手#xff0c;引入 「生产者消费者模型」#xff… 文章目录 前言一、v3版本二、单例模式概念特点简单实现 三、其余问题STL线程安全问题智能指针线程安全问题其他锁的概念 总结 前言 加油 一、v3版本 「优化版」从任务队列入手引入 「生产者消费者模型」同时引入 RAII 风格的锁实现自动化加锁与解锁 当前的 线程池 设计已经完成的差不多了接下来重点在于完善其他地方比如 任务队列及锁的优化 线程池 专注于 任务处理至于如何确保任务装载及获取时的线程安全问题交给 「生产者消费者模型」基于阻塞队列 就行了线程池v3版本的代码可以优化成下面这个样子
#pragma once#include vector
#include string
#include memory
#include functional
#include unistd.h
#include pthread.h#include Task.hpp
#include Thread.hpp
#include BlockingQueue.hpp // CP模型#define THREAD_NUM 10templateclass T
class ThreadPool
{using func_t std::functionvoid(T); // 包装器public:ThreadPool(func_t func, int num THREAD_NUM):_num(num), _func(func){}~ThreadPool(){// 等待线程退出for(auto t : _threads)t.join();}void init(){// 创建一批线程for(int i 0; i _num; i)_threads.push_back(Thread(i, threadRoutine, this));}void start(){// 启动线程for(auto t : _threads)t.run();}// 提供给线程的回调函数已修改返回类型为 voidstatic void threadRoutine(void *args){// 避免等待线程直接剥离pthread_detach(pthread_self());auto ptr static_castThreadPoolT*(args);while (true){// 从CP模型中获取任务T task ptr-popTask();task();ptr-callBack(task); // 回调函数}}// 装载任务void pushTask(const T task){_blockqueue.Push(task);}protected:func_t callBack(T task){_func(task);}T popTask(){T task;_blockqueue.Pop(task);return task;}private:std::vectorThread _threads;int _num; // 线程数量BlockQueueT _blockqueue; // 阻塞队列func_t _func;
};之前的 互斥锁、条件变量 相关操作交给 「生产者消费者模型」 处理线程池 不必关心关于 「生产者消费者模型」 的实现大家可自行参考我之前写的文章 《生产者与消费者模型》 手动 加锁、解锁 显得不够专业并且容易出问题比如忘记释放锁资源而造成死锁因此我们可以设计一个小组件 LockGuard实现 RAII 风格的锁初始化创建析构时销毁 将这个小组件加入 BlockingQueue.hpp 中可以得到以下代码
#pragma once#include queue
#include mutex
#include pthread.h
#include LockGuard.hpp// 命名空间避免冲突
#define DEF_SIZE 10templateclass T
class BlockQueue
{
public:BlockQueue(size_t cap DEF_SIZE):_cap(cap){// 初始化锁与条件变量pthread_mutex_init(_mtx, nullptr);pthread_cond_init(_pro_cond, nullptr);pthread_cond_init(_con_cond, nullptr);}~BlockQueue(){// 销毁锁与条件变量pthread_mutex_destroy(_mtx);pthread_cond_destroy(_pro_cond);pthread_cond_destroy(_con_cond);}// 生产数据入队void Push(const T inData){// 加锁RAII风格LockGuard lock(_mtx);// 循环判断条件是否满足while(IsFull()){pthread_cond_wait(_pro_cond, _mtx);}_queue.push(inData);// 可以加策略唤醒比如生产一半才唤醒消费者pthread_cond_signal(_con_cond);// 自动解锁}// 消费数据出队void Pop(T* outData){// 加锁RAII 风格LockGuard lock(_mtx);// 循环判读条件是否满足while(IsEmpty()){pthread_cond_wait(_con_cond, _mtx);}*outData _queue.front();_queue.pop();// 可以加策略唤醒比如消费完后才唤醒生产者pthread_cond_signal(_pro_cond);// 自动解锁}private:// 判断是否为满bool IsFull(){return _queue.size() _cap;}// 判断是否为空bool IsEmpty(){return _queue.empty();}private:std::queueT _queue;size_t _cap; // 阻塞队列的容量pthread_mutex_t _mtx; // 互斥锁pthread_cond_t _pro_cond; // 生产者条件变量pthread_cond_t _con_cond; // 消费者条件变量
};最后引入 main.cc并编译运行程序查看结果是否正确
#include ThreadPool.hpp
#include memorytypedef Taskint type;// 回调函数
void callBack(type task)
{// 获取计算结果后打印std::string ret task.getResult();std::cout 计算结果为: ret std::endl;
}int main()
{std::unique_ptrThreadPooltype ptr(new ThreadPooltype(callBack));ptr-init();ptr-start();// 还有后续动作while(true){// 输入 操作数 操作数 操作符int x 0, y 0;char op ;std::cout 输入 x: ;std::cin x;std::cout 输入 y: ;std::cin y;std::cout 输入 op: ;std::cin op;// 构建任务对象type task(x, y, op);// 装载任务ptr-pushTask(task);}return 0;
}二、单例模式
概念 代码构建类类实例化出对象这个实例化出的对象也可以称为 实例比如常见的 STL 容器在使用时都是先根据库中的类形成一个 实例 以供使用正常情况下一个类可以实例化出很多很多个对象但对于某些场景来说是不适合创建出多个对象的 比如本文中提到的 线程池当程序运行后仅需一个 线程池对象 来进行高效任务计算因为多个 线程池对象 无疑会大大增加调度成本因此需要对 线程池类 进行特殊设计使其只能创建一个 对象换句话说就是不能让别人再创建对象 在一个程序中只允许实例化出一个对象可以通过 单例模式 来实现单例模式 是非常 经典、常用、常考 的设计模式
特点 单例模式 最大的特点就是 只允许存在一个对象实例这就好比现在的 一夫一妻制 一样要是在古代单例模式 肯定不被推崇 在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百 GB) 到内存中此时往往要用一个 单例 的类来管理这些数据在我们今天的场景中也需要一个 单例线程池 来协同生产者与消费者
简单实现 单例模式 有两种实现方向饿汉 与 懒汉它们避免类被再次创建出对象的手段是一样的构造函数私有化、删除拷贝构造 只要外部无法访问 构造函数那么也就无法构建对象了比如下面这个类 Signal
#pragma once
33333333333333
#include iostreamclass Signal
{
private:// 构造函数私有化Signal(){}// 删除拷贝构造Signal(const Signal) delete;
};当外界试图创建对象时 当然这只实现了一半还有另一半是 创建一个单例对象既然外部受权限约束无法创建对象那么类内是肯定可以创建对象的只需要创建一个指向该类对象的 静态指针 或者一个 静态对象再初始化就好了因为外部无法访问该指针所以还需要提供一个静态函数 getInstance() 以获取单例对象句柄至于具体怎么实现需要分不同方向饿汉 or 懒汉 0 .
#pragma once#include iostreamclass Signal
{
private:// 构造函数私有化Signal(){}// 删除拷贝构造Signal(const Signal) delete;public:// 获取单例对象的句柄static Signal* getInstance(){return _sigptr;}void print(){std::cout Hello Signal! std::endl;}private:// 指向单例对象的静态指针static Signal *_sigptr;
};注意 构造函数不能只声明需要实现即使什么都不写 为什么要删除拷贝构造 如果不删除拷贝构造那么外部可以借助拷贝构造函数拷贝构造出一个与 单例对象 一致的 “对象”此时就出现两个对象这是不符合 单例模式 特点的 为什么要创建一个静态函数 单例对象也需要被初始化并且要能被外部使用 调用链逻辑通过静态函数获取句柄静态单例对象地址- 通过地址调用该对象的其他函数 现在我们来看下饿汉模式 在程序加载到内存时就已经早早的把 单例对象 创建好了此时程序服务还没有完全启动也就是在外部直接通过 new 实例化一个对象具体实现如下
#pragma once#include iostream// 饿汉模式
class Signal
{
private:// 构造函数私有化Signal(){}// 删除拷贝构造Signal(const Signal) delete;public:static Signal* getInstance(){return _sigptr;}void print(){std::cout Hello Signal! std::endl;}private:// 指向单例对象的静态指针static Signal* _sigptr;
};Signal* Signal::_sigptr new Signal();注在程序加载时该对象会被创建 这里的 单例对象 本质就有点像 全局变量在程序加载时就已经创建好了 外部可以直接通过 getInstance() 获取 单例对象 的操作句柄来调用类中的其他函数 main.cc #include iostream
#include Signal.hppint main()
{Signal::getInstance()-print();return 0;
}运行结果为 这就实现了一个简单的 饿汉版单例类除了创建 static Signal* 静态单例对象指针 外也可以直接定义一个 静态单例对象生命周期随进程不过要注意的是getInstance() 需要返回的也是该静态单例对象的地址不能返回值因为拷贝构造被删除了并且需要在类的外部初始化该静态单例对象
#pragma once#include iostream// 饿汉模式
class Signal
{
private:// 构造函数私有化Signal(){}// 删除拷贝构造Signal(const Signal) delete;public:static Signal *getInstance(){return _sig;}void print(){std::cout Hello Signal! std::endl;}private:// 静态单例对象static Signal _sig;
};// 初始化
Signal Signal::_sig;饿汉模式 是一个相对简单的单例实现方向只需要在类中声明在类外初始化就行了但它也会带来一定的弊端延缓服务启动速度 完全启动服务是需要时间的创建 单例对象 也是需要时间的饿汉模式 在服务正式启动前会先创建对象但凡这个单例类很大服务启动时间势必会受到影响大型项目启动时间就是金钱 并且由于 饿汉模式 每次都会先创建 单例对象再启动服务如果后续使用 单例对象 还好说但如果后续没有使用 单例对象那么这个对象就是白创建了在延缓服务启动的同时造成了一定的资源浪费 综上所述饿汉模式 不是很推荐使用除非图实现简单并且服务规模较小既然 饿汉模式 有缺点就需要改进于是就出现了 懒汉模式 现在我们来看下懒汉模式 在 懒汉模式 中单例对象 并不会在程序加载时创建而是在第一次调用时创建第一次调用创建后后续无需再创建直接使用即可
#pragma once#include iostream// 懒汉模式
class Signal
{
private:// 构造函数私有化Signal(){}// 删除拷贝构造Signal(const Signal) delete;public:static Signal *getInstance(){// 第一次调用才创建if(_sigptr nullptr){_sigptr new Signal();}return _sigptr;}void print(){std::cout Hello Signal! std::endl;}private:// 静态指针static Signal *_sigptr;
};// 初始化静态指针
Signal* Signal::_sigptr nullptr;注意 此时的静态指针需要初始化为 nullptr方便第一次判断 饿汉模式 中出现的问题这里全都避免了
创建耗时 - 只在第一次使用时创建占用资源 - 如果不使用就不会被创建 懒汉模式 的核心在于 延时加载可以优化服务器的速度及资源占用 延时加载这种机制就有点像 「写时拷贝」就赌你不会使用从而节省资源开销类似的还有 动态库、进程地址空间 等 当然懒汉模式 下也是可以正常使用 单例对象 的 这样看来懒汉模式 确实优秀实现起来也不麻烦为什么会说 饿汉模式 更简单呢 这是因为当前只是单线程场景程序暂时没啥问题如果当前是多线程场景问题就大了如果一批线程同时调用 getInstance()同时认定 _sigptr 为空就会创建多个 单例对象这是不合理的 也就是说当前实现的 懒汉模式 存在严重的线程安全问题 我们现在来证明一下 简单改一下代码每创建一个单例对象就打印一条语句将代码放入多线程环境中测试 获取单例对象句柄 getInstance() — 位于 Signal 类 static Signal *getInstance()
{// 第一次调用才创建if(_sigptr nullptr){std::cout 创建了一个单例对象 std::endl;_sigptr new Signal();}return _sigptr;
}源文件 main.cc #include test63.hpp
#include iostream
#include pthread.hint main()
{// 创建一批线程pthread_t arr[10];for(int i 0; i 10; i){pthread_create(arr i, nullptr, [](void*)-void*{// 获取句柄auto ptr Signal::getInstance();ptr-print();return nullptr;}, nullptr);}for(int i 0; i 10; i)pthread_join(arr[i], nullptr);return 0;
}当前代码在多线程环境中同时创建了多个 单例对象因此是存在线程安全问题的 没有因为饿汉模式下单例对象一开始就被创建了即便是多线程场景中也不会创建多个对象它们也做不到 所以现在我们利用线程互斥锁来保护单例对象的创建
#pragma once#include iostream
#include mutex// 懒汉模式
class Signal
{
private:// 构造函数私有化Signal(){}// 删除拷贝构造Signal(const Signal) delete;public:static Signal* getInstance(){// 加锁保护pthread_mutex_lock(_mtx);if(_sigptr nullptr){std::cout 创建了一个单例对象 std::endl;_sigptr new Signal();}pthread_mutex_unlock(_mtx);return _sigptr;}void print(){std::cout Hello Signal! std::endl;}private:// 静态指针static Signal *_sigptr;static pthread_mutex_t _mtx;
};// 初始化静态指针
Signal* Signal::_sigptr nullptr;// 初始化互斥锁
pthread_mutex_t Signal::_mtx PTHREAD_MUTEX_INITIALIZER;注意 getInstance() 是静态函数互斥锁也要定义为静态的可以初始化为全局静态锁 依旧是借助之前的多线程场景测试一下改进后的 懒汉模式 代码有没有问题 结果是没有问题单例对象 也只会创建一个 现在还面临最后一个问题效率问题 当前代码确实能保证只会创建一个 单例对象但即使后续不会创建 单例对象也需要进行 加锁、判断、解锁 这个流程要知道 加锁 也是有资源消耗的所以这种写法不妥 解决方法是在 加锁 前再来一次判断N个线程顶多只会进行 N次加锁或解锁真是极其优雅
static Signal *getInstance()
{// 双检查if(_sigptr nullptr){// 加锁保护pthread_mutex_lock(_mtx);if(_sigptr nullptr){std::cout 创建了一个单例对象 std::endl;_sigptr new Signal();}pthread_mutex_unlock(_mtx);}return _sigptr;
}单纯的 if 判断并不会消耗很多资源但 加锁 行为会消耗资源延缓程序运行速度双检查加锁 可以有效避免这个问题 值得一提的是懒汉模式 还有一种非常简单的新式写法调用 getInstance() 时创建一个静态单例对象并返回因为静态单例对象只会初始化一次所以是可行的并且在 C11 之后可以保证静态变量初始化时的线程安全问题也就不需要 双检查加锁 了实现起来非常简单
#pragma once#include iostream
#include mutex// 懒汉模式
class Signal
{
private:// 构造函数私有化Signal(){}// 删除拷贝构造Signal(const Signal) delete;public:static Signal *getInstance(){// 静态单例对象只会初始化一次并且生命周期随进程static Signal _sig;return _sig;}void print(){std::cout Hello Signal! std::endl;}
};注意 静态变量创建时的线程安全问题在 C11 之前是不被保障的 那之前的线程池你是否可以通过单例模式来进行最终版本优化 这就交给你来啦 三、其余问题
STL线程安全问题 STL库中的容器是否是线程安全的 答案是 不是 因为 STL 设计的初衷就是打造出极致性能容器而加锁、解锁操作势必会影响效率因此 STL 中的容器并未考虑线程安全在之前编写的 生产者消费者模型、线程池 中使用了部分 STL 容器如 vector、queue、string 等这些都是需要我们自己去加锁、解锁以确保多线程并发访问时的线程安全问题 从另一方面来说STL 容器种类繁多容器间实现方式各不相同无法以统一的方式进行加锁、解锁操作比如哈希表中就有 锁表、锁桶 两种方式 所以在多线程场景中使用 STL 库时需要自己确保线程安全
智能指针线程安全问题 C 标准提供的智能指针有三种unique_ptr、shared_ptr、weak_ptr 首先来说 unique_ptr这是个功能单纯的智能指针只具备基本的 RAII 风格不支持拷贝因此无法作为参数传递也就不涉及线程安全问题 其次是 shared_ptr得益于 引用计数这个智能指针支持拷贝可能被多线程并发访问但标准库在设计时考虑到了这个问题索性将 shared_ptr 对于引用计数的操作设计成了 原子操作 CAS这就确保了它的 线程安全至于 weak_ptr这个就是 shared_ptr 的小弟名为弱引用智能指针具体实现与 shared_ptr 一脉相承因此它也是线程安全的
其他锁的概念 悲观锁总是认为数据会被其他线程修改于是在自己访问数据前会先加锁其他线程想访问时只能等待之前使用的锁都属于悲观锁 乐观锁并不认为其他线程会来修改数据因此在访问数据前并不会加锁但是在更新数据前会判断其他数据在更新前有没有被修改过主要通过 版本号机制 和 CAS操作实现 CAS 操作当需要更新数据时会先判断内存中的值与之前获取的值是否相等如果相等就用新值覆盖旧值失败就不断重试 自旋锁申请锁失败时线程不会被挂起而且不断尝试申请锁 自旋 本质上就是一个不断 轮询 的过程即不断尝试申请锁这种操作是十分消耗 CPU 时间的因此推荐临界区中的操作时间较短时使用 自旋锁 以提高效率操作时间较长时自旋锁 会严重占用 CPU 时间 自旋锁 的优点可以减少线程切换的消耗
#include pthread.hpthread_spinlock_t lock; // 自旋锁类型int pthread_spin_init(pthread_spinlock_t *lock, int pshared); // 初始化自旋锁int pthread_spin_destroy(pthread_spinlock_t *lock); // 销毁自旋锁// 自旋锁加锁
int pthread_spin_lock(pthread_spinlock_t *lock); // 失败就不断重试阻塞式
int pthread_spin_trylock(pthread_spinlock_t *lock); // 失败就继续向后运行非阻塞式// 自旋锁解锁
int pthread_spin_unlock(pthread_spinlock_t *lock);就这接口风格跟 mutex 互斥锁 是一脉相承可以轻易上手将 线程池 中的 互斥锁 轻易改为 自旋锁 这就留到我们下篇再来介绍吧~本篇写累了想结束了 总结 要结束喽~