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

embedded/2024/11/27 14:08:47/

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

💗系列专栏: 【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指针 + staticint 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++; // 防止阻塞,因为一开始休眠线程数为0LOG(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 能够高效, 原子的操作引用计数.


http://www.ppmy.cn/embedded/140913.html

相关文章

Android BottomNavigationView 底部导航栏使用详解

一、BottomNavigationView简介 BottomNavigationView是官方提供可以实现底部导航的组件&#xff0c;最多支持5个item&#xff0c;主要用于功能模块间的切换&#xff0c;默认会包含动画效果。 官方介绍地址&#xff1a;BottomNavigationView 二、使用BottomNavigationView a…

SpringBoot(四十)SpringBoot集成RabbitMQ使用过期时间+死信队列实现延迟队列

前边我们使用RabbitMQ实现了高并发下对流量的削峰填谷。正常在实际应用中大概也就够用了。 有的时候呢&#xff0c;我们需要使用到延迟队列&#xff0c;RabbitMQ不像RocketMQ一样默认就支持延迟队列&#xff0c;RabbitMQ是不支持延迟队列的&#xff0c;但是呢&#xff1f;我们可…

OCR-free Document Understanding Transformer

摘要:理解文档图像(如发票)是一个核心且具有挑战性的任务,因为它需要执行复杂的功能,如读取文本和对文档的整体理解。目前的视觉文档理解(VDU)方法将读取文本的任务外包给现成的光学字符识别(OCR)引擎,并专注于使用OCR输出进行理解任务。尽管基于OCR的方法显示出令人…

c++编程玩转物联网:使用芯片控制8个LED实现流水灯技术分享

在嵌入式系统中&#xff0c;有限的GPIO引脚往往限制了硬件扩展能力。74HC595N芯片是一种常用的移位寄存器&#xff0c;通过串行输入和并行输出扩展GPIO数量。本项目利用树莓派Pico开发板与74HC595N芯片&#xff0c;驱动8个LED实现流水灯效果。本文详细解析项目硬件连接、代码实…

极狐GitLab 17.6 正式发布几十项与 DevSecOps 相关的功能【二】

GitLab 是一个全球知名的一体化 DevOps 平台&#xff0c;很多人都通过私有化部署 GitLab 来进行源代码托管。极狐GitLab 是 GitLab 在中国的发行版&#xff0c;专门为中国程序员服务。可以一键式部署极狐GitLab。 学习极狐GitLab 的相关资料&#xff1a; 极狐GitLab 官网极狐…

零基础3分钟快速掌握 ——Linux【终端操作】及【常用指令】Ubuntu

1.为啥使用Linux做嵌入式开发 能广泛支持硬件 内核比较高效稳定 原码开放、软件丰富 能够完善网络通信与文件管理机制 优秀的开发工具 2.什么是Ubuntu 是一个以桌面应用为主的Linux的操作系统&#xff0c; 内核是Linux操作系统&#xff0c; 具有Ubuntu特色的可视…

接上一主题,C++14中如何设计类似于std::any,使集合在C++中与Python一样支持任意数据?

这篇文章的重点是C多态的应用&#xff0c;但是如果你是C新手&#xff0c; 你需要了解以下C知识&#xff1a; 类 构造函数 拷贝构造函数 虚拟函数 纯虚拟函数 析构函数 类的继承 运算符重写 模板类 模板参数 数组 数组的传递 指针与动态内存分配 Python&#xff1a; s …

前端开发工程师需要学什么?

‌前端开发工程师需要学习的主要内容包括HTML、CSS、JavaScript、前端框架、响应式设计、性能优化、版本控制等。‌ HTML/CSS/JavaScript ‌HTML‌&#xff1a;是网页的骨架&#xff0c;负责网页的结构和内容。‌CSS‌&#xff1a;用于美化网页&#xff0c;设计样式和布局。‌…