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

server/2024/11/27 12:07:18/

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

💗系列专栏: 【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/server/145328.html

相关文章

ansible playbook安装nacos

目录 1.创建nacos应用相关的用户 2.创建rocketmq所需目录任务 3.安装JDK1.8、python以及虚拟环境安装PyMysql(用于执行数据库脚本) 4. 解压nacos 5.创建nacos数据库 6.上传sql文件 7.创建数据库表 8.启动nacos 9.nacos集群主机ip配置 10.变量配置 11.配置nacos.yml 更…

C# 结构体

文章目录 前言一、结构体的定义与基本使用&#xff08;一&#xff09;定义结构体&#xff08;二&#xff09;结构体的使用示例 二、C# 结构的特点&#xff08;一&#xff09;丰富的成员类型&#xff08;二&#xff09;构造函数相关限制与特性&#xff08;三&#xff09;继承方面…

【Android】android compat理解

1&#xff0c;前提 即便是在同一手机上安装的不同apk&#xff0c;其编译的apk不同&#xff0c;也会导致行为上的差异。如SDK34有限制后台启动&#xff0c;但如果安装的apk所依赖的sdk是33&#xff0c;则不会表现出此差异。这是如何实现的呢&#xff1f;其实&#xff0c;本质是…

分布式kettle调度平台v6.4.0新功能介绍

介绍 Kettle&#xff08;也称为Pentaho Data Integration&#xff09;是一款开源的ETL&#xff08;Extract, Transform, Load&#xff09;工具&#xff0c;由Pentaho&#xff08;现为Hitachi Vantara&#xff09;开发和维护。它提供了一套强大的数据集成和转换功能&#xff0c…

day01

Hm-Footer.vue <template><div class"hm-footer">我是hm-footer</div></template><script>export default {}</script><style>.hm-footer{height:100px;line-height:100px;text-align:center;font-size:30px;background-…

Java学习,重载(Overload)

Java中&#xff0c;重载&#xff08;Overload&#xff09;是指同一个类中&#xff0c;可以有多个方法有相同的名称&#xff0c;但这些方法的参数列表必须不同。参数列表的不同可以体现在参数的个数、参数的类型或者参数的顺序上。重载使得一个类可以以多种方式被调用&#xff0…

解决 node.js 执行 npm下载 报无法执行脚本的错

报错如下 这个问题其实就是本地策略组造成的 策略的查看是 Get-ExecutionPolicy -List 看一下本地策略是什么情况 正常能 执行脚本 的情况如下&#xff08;这个就是微软的安全策略&#xff09; 如果你的这些全是 Undefined那就中招了 Undefined 当前 scope中没有…

用天翼云搭建一个HivisionIDPhoto证件照处理网站

世人不必记我&#xff0c;我不记世人。 HivisionIDPhoto证件照处理网站 世人不必记我&#xff0c;我不记世人。项目地址项目搭建与修改前端后端遇到的坑 成果图 前段时间工作需要频繁处理证件照&#xff0c;当时同事推荐一个证件照小程序&#xff08;要看广告&#xff09;&…