【Linux系统编程】第五十弹---构建高效单例模式线程池、详解线程安全与可重入性、解析死锁与避免策略,以及STL与智能指针的线程安全性探究

【Linux系统编程】第五十弹---构建高效单例模式线程池、详解线程安全与可重入性、解析死锁与避免策略,以及STL与智能指针的线程安全性探究

✨个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】

目录

1、将日志加到线程池

1.1、Thread类

1.2、ThreadPool类

1.2.1、HandlerTask()

1.2.2、其他公有成员函数

1.3、主函数

2、单例版线程池

2.1、私有成员函数

2.2、获取对象函数

2.2.1、不加锁版本

2.2.2、加锁版本

3、可重入VS线程安全

3.1、概念

3.2、常见的线程不安全的情况

3.3、常见的线程安全的情况

3.4、常见不可重入的情况

3.5、常见可重入的情况

3.6、可重入与线程安全联系

3.7、可重入与线程安全区别

4、常见锁概念

4.1、死锁

4.2、死锁四个必要条件

4.3、避免死锁

4.4、避免死锁算法

5、STL,智能指针和线程安全


1、将日志加到线程池

1.1、Thread类

此处使用日志来打印消息,因此需要将Thread类的std::cout打印信息删除掉!!!
#pragma once #include <iostream> #include <string> #include <functional> #include <pthread.h> namespace ThreadMoudle { // typedef std::function<void()> func_t; using func_t = std::function<void(const std::string&)>; class Thread { public: void Excute() { _isrunning = true; _func(_name); _isrunning = false; } public: Thread(const std::string& name,func_t func):_name(name),_func(func) {} // 新线程执行该方法 static void* ThreadRoutine(void* args) { Thread* self = static_cast<Thread*>(args); self->Excute(); return nullptr; } std::string Status() { if(_isrunning) return "running"; else return "sleep"; } bool Start() { // ::使用库函数接口,直接使用ThreadRoutine会报错,因为成员函数有隐含this指针 + static int n = ::pthread_create(&_tid,nullptr,ThreadRoutine,this); if(n != 0) return false; return true; } void Stop() { if(_isrunning) { ::pthread_cancel(_tid); _isrunning = false; } } std::string Name() { return _name; } void Join() { ::pthread_join(_tid,nullptr); } ~Thread() {} private: std::string _name; pthread_t _tid; bool _isrunning; func_t _func; // 线程要执行的回调函数 }; }

1.2、ThreadPool类

1.2.1、HandlerTask()

将std::cout部分改为LOG打印!!!
// 处理任务,从任务队列取任务,加锁 void HandlerTask(const std::string &name) { while (true) { // 1.取任务 LockQueue(); // 队列为空且运行则休眠 while (IsEmpty() && _isrunning) // if? { _sleep_thread_num++; // 防止阻塞,因为一开始休眠线程数为0 LOG(INFO,"%s thread sleep begin!\n",name.c_str()); Sleep(); LOG(INFO,"%s thread wakeup!\n",name.c_str()); _sleep_thread_num--; } // 判定一种情况,为空且不运行则退出 if (IsEmpty() && !_isrunning) { // std::cout << name << " quit" << std::endl; LOG(INFO,"%s thread quit!\n",name.c_str()); UnlockQueue(); break; } // 有任务 T t = _task_queue.front(); _task_queue.pop(); UnlockQueue(); // 2.处理任务 t(); // 处理任务(只属于自己线程),此处不用/不能在临界区处理 // std::cout << name << ":" << t.result() << std::endl; LOG(DEBUG,"hander task done,task is : %s\n",t.result().c_str()); } }

1.2.2、其他公有成员函数

此处只展示需要修改的部分!!!
void Init() { func_t func = std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1); // 绑定 for (int i = 0; i < _thread_num; i++) { std::string threadname = "thread-" + std::to_string(i + 1); // _threads.emplace_back(threadname,test/*TODO*/); // 按照构造函数尾插对象 _threads.emplace_back(threadname, func); // _threads.emplace_back(threadname, HandlerTask); LOG(DEBUG,"construct thread %s done,init success\n",threadname.c_str()); } } void Start() { _isrunning = true; for (auto &thread : _threads) { LOG(DEBUG,"start thread %s done.\n",thread.Name().c_str()); thread.Start(); } } void Stop() { LockQueue(); _isrunning = false; WakeupAll(); // 防止有线程在等待 UnlockQueue(); LOG(INFO,"thread pool stop done!\n"); }

1.3、主函数

主函数执行5次之后,停止线程池!!!
// 测试日志版线程池 int main() { ThreadPool<Task>* tp = new ThreadPool<Task>(); tp->Init(); tp->Start(); int cnt = 5; // 执行5次循环结束 while(cnt) { Task t(1,1); tp->Equeue(t); LOG(INFO,"equeue a task,%s\n",t.debug().c_str()); cnt--; sleep(1); } // 新线程停止 tp->Stop(); LOG(INFO,"thread pool stop\n"); // 5秒后主线程结束 sleep(5); return 0; }

运行结果 

2、单例版线程池

1、单例模式特点:某些类, 只应该具有一个对象(实例), 就称之为单例.

2、单例版(懒汉模式)线程池需要加两个成员变量静态线程池指针(获取类对象地址),静态互斥锁(解决多线程获取多个对象问题)!!!

注意:静态成员变量需要在类外初始化!!!

2.1、私有成员函数

为了实现单例模式需要将构造函数私有禁止拷贝构造和赋值操作符重载,并将初始化和启动线程池函数私有!!!
private: // 单例模式构造函数私有 ThreadPool(int thread_num = gdefaultnum) : _thread_num(thread_num), _isrunning(false), _sleep_thread_num(0) { pthread_mutex_init(&_mutex, nullptr); pthread_cond_init(&_cond, nullptr); } void Init() { func_t func = std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1); // 绑定 for (int i = 0; i < _thread_num; i++) { std::string threadname = "thread-" + std::to_string(i + 1); // _threads.emplace_back(threadname,test/*TODO*/); // 按照构造函数尾插对象 _threads.emplace_back(threadname, func); // _threads.emplace_back(threadname, HandlerTask); LOG(DEBUG, "construct thread %s done,init success\n", threadname.c_str()); } } void Start() { _isrunning = true; for (auto &thread : _threads) { LOG(DEBUG, "start thread %s done.\n", thread.Name().c_str()); thread.Start(); } } ThreadPool(const ThreadPool<T> &) = delete; // 禁止拷贝构造 void operator=(const ThreadPool<T> &) = delete; // 禁止赋值重载

2.2、获取对象函数

类外初始化静态成员

// 静态成员类外初始化 template <typename T> ThreadPool<T> *ThreadPool<T>::_tp = nullptr; template <typename T> pthread_mutex_t ThreadPool<T>::_sig_mutex = PTHREAD_MUTEX_INITIALIZER;

2.2.1、不加锁版本

单进程单线程可以不用加锁!!!
// 单线程 static ThreadPool<T> *GetInstance() { if (_tp == nullptr) { LOG(INFO, "create thread pool\n"); _tp = new ThreadPool(); _tp->Init(); _tp->Start(); } else { LOG(INFO, "get thread pool\n"); } return _tp; }

运行结果

测试是否为单例 

int main() { // 测试是否为单例 ThreadPool<Task>* tp1 = new ThreadPool<Task>(); ThreadPool<Task> tp2 = *(ThreadPool<Task>::GetInstance()); return 0; }

运行结果 

2.2.2、加锁版本

多线程可能会有创建多个对象的情况,需加锁解决!!!
static ThreadPool<T> *GetInstance() { // 只有第一次需要加锁,减少锁的竞争 if (_tp == nullptr) { LockGuard lockguard(&_sig_mutex); if (_tp == nullptr) { LOG(INFO, "create thread pool\n"); _tp = new ThreadPool(); _tp->Init(); _tp->Start(); } else { LOG(INFO, "get thread pool\n"); } } return _tp; }

3、可重入VS线程安全

3.1、概念

  • 线程安全多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

3.2、常见的线程不安全的情况

  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

3.3、常见的线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

3.4、常见不可重入的情况

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 可重入函数体内使用了静态的数据结构

3.5、常见可重入的情况

  • 不使用全局变量或静态变量
  • 不使用用 malloc 或者 new 开辟出的空间
  • 不调用不可重入函数
  • 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

3.6、可重入与线程安全联系

  • 函数是可重入的,那就是线程安全的
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的

3.7、可重入与线程安全区别

  • 可重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的

4、常见锁概念

4.1、死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

4.2、死锁四个必要条件

死锁就一定会出现下面的四个条件,但是出现下面四个条件不代表死锁!!!
  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

4.3、避免死锁

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

4.4、避免死锁算法

  • 死锁检测算法(了解)
  • 银行家算法(了解)

5、STL,智能指针和线程安全

STL 中的容器是否是线程安全的?

不是。原因是:

1、STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.
2、而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如 hash 表的锁表和锁桶).
因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.

智能指针是否是线程安全的?

1、对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.
2、对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.

Read more

学 C++ 继承看这篇!派生类函数实现 + 虚继承原理 + IO 库菱形案例,果断收藏

学 C++ 继承看这篇!派生类函数实现 + 虚继承原理 + IO 库菱形案例,果断收藏

✨ 孤廖:个人主页 🎯 个人专栏:《C++:从代码到机器》 🎯 个人专栏:《Linux系统探幽:从入门到内核》 🎯 个人专栏:《算法磨剑:用C++思考的艺术》 折而不挠,中不为下 文章目录 * 正文: * 1. 继承的概念和定义 * 概念: * 定义: * 继承类模板 * 2. 基类和派⽣类间的转换 * 3. 继承中的作⽤域' * 隐藏规则: * 4. 派⽣类的默认成员函数 * 4个常⻅默认成员函数 * 实现一个不能被继承的类 * 5. 继承与友元 * 6. 继承与静态成员 * 7. 多继承及其菱形继承问题 * 继承模型: * 虚继承 * 8. 继承和组合 * 结语: 正文: 1. 继承的概念和定义 概念:

By Ne0inhk
C++轻量客户端+浏览器:优雅的本地文件共享解决方案

C++轻量客户端+浏览器:优雅的本地文件共享解决方案

2025年12月31日 界面更新 在日常工作和学习中,你是否经常遇到这样的场景: * 需要在手机和平板上查看电脑里的文件 * 想给同事快速分享一个大文件,但网盘太慢 * 需要从不同设备访问同一个项目目录 * 想在局域网内搭建简单的文件服务器,但又不想配置复杂的FTP或Samba 今天给大家推荐一款我开发的小工具——DirectoryServer,一个基于Windows 11现代风格GUI的目录共享服务器,让你通过网页浏览器就能轻松访问本地文件! 功能亮点 🎨 现代化Windows 11界面 * 原生Windows体验:遵循Windows 11设计语言,完美融入系统 * 深色模式支持:自动适应系统主题,夜间使用更舒适 * 简洁直观:没有复杂设置,一键启动,即刻使用 🌐 跨平台文件访问 * 网页浏览器访问:任何设备(手机、平板、其他电脑)通过浏览器即可访问 * 无需安装客户端:访问端无需任何特殊软件 * 实时目录浏览:像使用本地文件管理器一样浏览远程目录 ⚡ 高效便捷 * 一键启动/停止:简单的开始和停止按钮 * 自动URL生

By Ne0inhk

CCF-GESP计算机学会等级考试2025年12月四级C++T2 优先购买

B4452 [GESP202512 四级] 优先购买 题目描述 小 A 有 MMM 元预算。商店有 NNN 个商品,每个商品有商品名 SSS、价格 PPP 和优先级 VVV 三种属性,其中 VVV 为正整数,且 VVV 越小代表商品的优先级越高。 小 A 的购物策略为: * 总是优先买优先级最高的东西; * 如果有多个最高优先级商品,购买价格最低的; * 如果有多个优先级最高且价格最低的商品,购买商品名字典序最小的。 小 A 想知道能购买哪些商品。 输入格式 第一行两个正整数 M,NM, NM,N,代表预算和商品数。 之后 NNN 行,每行一个商品,依次为 Si

By Ne0inhk

从前序和中序遍历重建二叉树 —— C++ 递归 + 哈希表写法解析

从前序和中序遍历重建二叉树 —— C++ 递归 + 哈希表写法解析 一、题目背景 已知一棵二叉树的: * 前序遍历序列preorder * 中序遍历序列inorder 并且树中不存在重复节点值,要求 重建这棵二叉树,返回根节点 TreeNode*。 二、关键性质回顾 二叉树的遍历有这些性质(这里用到前序 + 中序): 1. 前序遍历(Preorder): 顺序是:根节点 -> 左子树 -> 右子树  所以 preorder[0] 一定是整棵树的 根节点。 2. 中序遍历(Inorder): 顺序是:左子树 -> 根节点 -> 右子树 在中序序列中,根节点左边的部分是“左子树”,右边的部分是“

By Ne0inhk