【Linux系统编程】第五十弹---构建高效单例模式线程池、详解线程安全与可重入性、解析死锁与避免策略,以及STL与智能指针的线程安全性探究
✨个人主页: 熬夜学编程的小林
💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】
目录
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 能够高效, 原子的操作引用计数.
原文地址:https://blog.csdn.net/2201_75584283/article/details/142927168
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!