并发和并行
并行的每条核上采取的是并发的模式且同一时间不同任务能在不同的核上运行
IO密集型程序:程序指令的执行涉及一些IO操作,比如文件,设备,网络操作(IO操作会阻塞程序,会导致CPU空闲,造成资源浪费)
CPU密集型程序:程序的指令都是做计算用的
多线程程序设计问题
IO密集型和CPU密集型都适合设计成多线程程序(CPU密集型可以被CPU调度 ,IO密集型有阻塞队列也不会影响CPU利用率)
IO密集型适合设计成单核
CPU密集型适合设计成多核,不适合设计成单核,线程调度会加大CPU开销
线程是不是越多越好
- 线程的创建和销毁都是非常 重 的操作
- 线程栈本身占用大量内存
- 线程的上下文切换要占用大量时间
- 大量线程同时唤醒会使系统经常出现锯齿状负载(负载随时间变化的波动性或不稳定性,可能会影响系统的稳定性和性能)或者瞬间负载量很大导致宕机
线程池模式
fixed模式线程池
线程池里面的线程个数是固定不变的,一般指ThreadPool创建时根据当前机器的CPU核心数量进行指定
cached模式线程池
线程池里面的线程个数是可动态增长的,根据任务的数量动态的增加线程的数量,但是会设置一个线程数量的阈值,任务处理完成,如果动态增长的线程空闲了60s还没有处理其他任务,那么关闭线程,保持池中最初的数量线程即可
生产者消费者模型
cond_wait :1.改变了线程的状态(等待) 释放资源2.释放了mtx锁
cond.notify_all: 将处于等待态的线程转为阻塞态
使用 unique_lock 不使用 lock_guard 原因(二者区别):
生产者消费者模型需要手动解锁,lock_guard没有提供手动解锁功能
不同情况分析:
-
生产消费动态平衡,不会触发cond_wait(lock),例如:
生产者首先抢到互斥锁,生产商品后 cond.notify_all 出作用域 释放互斥锁
进入消费者消费阶段 消费商品后 cond.notify_all 出作用域 释放互斥锁
此时生产者 消费者 获取互斥锁的权限由操作系统调度 -
临界区资源过剩或过少,例如:
生产者 while(full()) cond.wait(mtx) 之后首先将自己转为 等待态 然后释放 互斥锁 ,因为此时生产者线程处于等待态 ,只能由 消费者 抢夺互斥锁,消费者消费商品后 cond.notify_all,此操作会将生产者由等待态转换成阻塞态,直到放锁后生产者消费者都进入就绪态 (一旦锁被释放,操作系统会将等待该锁的线程从阻塞态转换到就绪态,参与CPU调度以继续执行)
- 等待态/阻塞态的线程不会主动争夺互斥锁,而是被动地等待互斥锁的释放。
- 当互斥锁被释放时,等待该锁的线程会被唤醒,进入就绪态,准备再次争夺互斥锁
线程池框架
ThreadPool及线程接口定义
class Thread
{
public:
//线程函数对象类型
using ThreadFunc = std::function<void()>;
//线程构造
Thread(ThreadFunc func);
//线程析构
~Thread();
//启动线程
void start();
private:
ThreadFunc func_;
};
class ThreadPool
{
public:
//线程池构造
ThreadPool();
//线程池析构
~ThreadPool();
//设置线程池的工作模式
void setMode(PoolMode mode);
//设置task任务队列上限阈值
void setTaskQueMaxThreshHold(int threshhold);
//给线程池提交任务
Result submitTask(std::shared_ptr<Task> sp);
//开启线程池
void start(int initThreadSize = 4);
ThreadPool(const ThreadPool&) = delete;
ThreadPool& operator=(const ThreadPool&) = delete;
private:
//定义线程函数
void ThreadFunc();
private:
std::vector<std::unique_ptr <Thread>> threads_; //线程列表
size_t initThreadSize_; //初始化线程数量
std::queue<std::shared_ptr<Task>> taskQue_; //任务队列
std::atomic_int taskSize_; //任务数量
int taskQueMaxThreshHold_; //任务队列数量上限阈值
std::mutex taskQueMtx_; //保证任务队列的线程安全
std::condition_variable notFull_; //表示任务队列不满
std::condition_variable notEmpty_; //表示任务队列不空
PoolMode poolMode_; //当前线程池的工作模式
};
ThreadPool及线程方法实现
ThreadPool
#include"threadpool.h"
#include<functional>
#include<thread>
#include<iostream>
const int TASK_MAX_THRESHHOLD = 1024;
//线程池构造
ThreadPool::ThreadPool()
:initThreadSize_(0)
,taskSize_(0)
,taskQueMaxThreshHold_(TASK_MAX_THRESHHOLD)
,poolMode_(PoolMode::MODE_FIXED)
{}
//线程池析构
ThreadPool::~ThreadPool()
{}
//设置线程池的工作模式
void ThreadPool::setMode(PoolMode mode)
{
poolMode_ = mode;
}
//设置task任务队列上限阈值
void ThreadPool::setTaskQueMaxThreshHold(int threshhold)
{
taskQueMaxThreshHold_ = threshhold;
}
//给线程池提交任务 用户调用该接口,传入任务对象,生产任务
Result ThreadPool::submitTask(std::shared_ptr<Task> sp)
{
//获取锁
std::unique_lock<std::mutex> lock(taskQueMtx_);
//线程的通信 等待任务队列有空余 wait wait_for wait_until
//用户提交任务 最长不能阻塞超过1s 否则判断提交任务失败 返回
if(!notFull_.wait_for(lock,std::chrono::seconds(1),
[&]()->bool {return taskQue_.size() < (size_t)taskQueMaxThreshHold_;}))
{
//表示notFull_等待1s,条件依然没有满足
std::cerr <<"task queue is full,submit task fail." <<std::endl;
return Result(sp,false);
}
//如果有空余 把任务放入任务队列
taskQue_.emplace(sp);
taskSize_++;
//新放了任务 任务队列不为空 在notEmpty_上进行通知 分配线程执行任务
notEmpty_.notify_all();
//需要根据任务数量和空闲线程数量 判断是否需要创建新的线程
//返回任务的Result对象
return Result(sp);
}
//开启线程池
void ThreadPool::start(int initThreadSize)
{
//记录初始线程个数
initThreadSize_ = initThreadSize;
//创建线程对象
for(int i = 0;i < initThreadSize_;i++)
{
//创建thread线程对象的时 把线程函数给到thread线程对象
auto ptr = std::make_unique<Thread>(std::bind(&ThreadPool::ThreadFunc,this));
threads_.emplace_back(std::move(ptr));
}
//启动所有线程 std::vector<Thread*> threads_;
for(int i = 0;i < initThreadSize_;i++)
{
threads_[i]->start();
}
}
//定义线程函数 线程池的所有线程从任务队列里面消费任务
void ThreadPool::ThreadFunc()
{
for(;;)
{
std::shared_ptr<Task> task;
{
//先获取锁
std::unique_lock<std::mutex> lock(taskQueMtx_);
std::cout << "tid:" <<std::this_thread::get_id()
<< "尝试获取新任务..." <<std::endl;
//cached模式下 可能已经创建了很多线程 但是空闲时间超过60s 应该把多余的线程回收掉
//等待notEmpty条件
notEmpty_.wait(lock,[&]()-> bool {return taskQue_.size() > 0;});
//从任务队列中取一个任务出来
auto task = taskQue_.front();
taskQue_.pop();
taskSize_--;
//如果依然有剩余任务 继续通知其他的线程执行任务
if(taskQue_.size() > 0)
{
notEmpty_.notify_all();
}
//取出一个任务 进行通知 通知可以继续提交生产任务
notFull_.notify_all();
} //此时应该释放锁
//当前线程负责执行这个任务
if(task != nullptr)
{
// task->run(); //执行任务:把任务的返回值setVal方法给到Result
task->exec();
}
}
}
线程函数及任务类
//线程构造
Thread::Thread(ThreadFunc func)
:func_(func){}
// 启动线程
void Thread::start()
{
//创建一个线程来执行线程函数
std::thread t(func_); //C++11 线程对象t和线程函数func_
t.detach(); //设置分离线程 pthread_detach
}
Task::Task()
:result_(nullptr)
{}
void Task::exec()
{
if(result_ != nullptr)
{
result_->setVal(run()); //发生多态调用
}
}
void Task::setResult(Result* res)
{
result_ = res;
}
任务处理返回值设计
任务返回值问题思考
class MyTask: public Task
{
public:
MyTask(int begin,int end)
:begin_(begin)
,end_(end)
{}
//如何设计run函数的返回值,可以表示任意类型?
void run()
{
std::cout << "tid:" << std::this_thread::get_id()
<< "begin!" << std::endl;
int sum = 0;
for(int i = begin_;i <= end_;i++)
sum += i;
std::cout << "tid:" << std::this_thread::get_id() << "end!" << std::endl;
return sum; //返回一个整型sum
}
private:
int begin_;
int end_;
};
int main()
{
ThreadPool pool;
pool.start(4);
//不同的任务拥有不同的返回值,如何设计这里的Result机制,使得任务返回值能被正确接收?
Result res = pool.submitTask(std::make_shared<MyTask>());
int sum = res.get().cast_<T>(); //get返回了一个Any类型的值,如何转为具体的类型?
pool.submitTask(std::make_shared<MyTask>());
pool.submitTask(std::make_shared<MyTask>());
}
Any类设计
构造一个Any类,可以接收任意数据的类型
class Any
{
public:
Any() = default;
~Any() = default;
Any(const Any&) = delete;
Any& operator=(const Any&) = delete;
Any(Any&&) = default;
Any& operator=(Any&&) = default;
//此构造函数可以让Any类型接收任意其他类型的数据
template<typename T>
Any(T data): base_(std::make_unique<Derive<T>>(data))
{}
//此方法可以将Any对象里存储的data数据提取出来
template<typename T>
T cast_()
{
//如何在base_找到其所指向的Derive对象,从中提取出data成员变量
//基类指针 -> 派生类指针 RTTI
Derive<T>* pd = dynamic_cast<Derive<T>*>(base_.get());
if(pd == nullptr)
{
throw "type is unmatch!";
}
return pd->data_;
}
private:
//基类类型
class Base
{
public:
virtual ~Base() = default;
};
//派生类类型
template<typename T>
class Derive: public Base
{
public:
Derive(T data): data_(data)
{}
private:
T data_;
};
private:
//定义一个基类指针
std::unique_ptr<Base> base_;
};
semaphore信号量实现
使用get方法获取任务返回值存在的问题:任务提交线程和任务处理线程不同步
解决方法:使用semaphore信号量
// 实现一个信号量类
class Semaphore
{
public:
Semaphore(int limit = 0)
:resLimit_(limit)
{}
~Semaphore() = default;
//获取一个信号量资源
void wait()
{
std::unique_lock<std::mutex> lock(mtx_);
//等待信号量有资源,如果没有则阻塞当前线程
cond_.wait(lock,[&]()->bool {return resLimit_ > 0;});
resLimit_--;
}
//增加一个信号量资源
void post()
{
std::unique_lock<std:mutex> lock(mtx_);
resLimit_++;
cond_.notify_all();
}
private:
int resLimit_;
std::mutex mtx_;
std::condition_variable cond_;
};
返回值Result类实现
//任务抽象基类
class Task
{
public:
void exec();
void setResult(Result* res);
//用户可以自定义任意任务类型,从Task继承,重写run方法,实现自定义任务处理
virtual Any run() = 0;
private:
Result* result_; //使用裸指针,避免强智能指针的交叉引用 Result对象生命周期 > task
};
//Task类型的前置声明
class Task;
//实现接受提交到线程池的task任务执行完成后的返回值类型Result
class Result
{
public:
Result(std::shared_ptr<Task> task,bool isValid = true);
~Result() = default;
//setVal方法,获取任务执行完的返回值
void setVal(Any any);
//get方法,用户调用这个方法获取task的返回值
Any get();
private:
Any any_; //存储任务返回值
Semaphore sem_; //线程通信信号量
std::shared_ptr<Task> task_; //指向对应获取返回值的任务对象
std::atomic_bool isValid_; //返回值是否有效
};
Result方法实现:
Result:: Result(std::shared_ptr<Task> task,bool isValid)
:isValid_(isValid)
,task_(task)
{
task_->setResult(this);
}
Any Result::get() //用户调用
{
if(!isValid)
{
return "";
}
sem_.wait(); //task任务如果没有执行完,至此会阻塞用户线程
return std::move(any_);
}
void Result::setVal(Any any)
{
//存储task返回值
this->any_ = std::move(any);
sem_.post(); //已经获取任务的返回值,增加信号量资源
}
线程池Cache模式实现
//定义线程函数 线程池的所有线程从任务队列里面消费任务
void ThreadPool::ThreadFunc(int threadid) //线程函数返回,相应的线程也就结束了
{
auto lastTime = std::chrono::high_resolution_clock().now();
while(isPoolRunning_)
{
std::shared_ptr<Task> task;
{
//先获取锁
std::unique_lock<std::mutex> lock(taskQueMtx_);
std::cout << "tid:" <<std::this_thread::get_id()
<< "尝试获取新任务..." <<std::endl;
//cached模式下 可能已经创建了很多线程 但是空闲时间超过60s 应该把多余的线程回收掉
//结束回收掉 (超过initThreadSize_数量的线程要进行回收)
//当前时间 - 上一次线程执行的时间 > 60s
//每一秒钟返回一次 怎么区分:超时返回 or 有任务待执行返回
while(taskQue_.size() == 0)
{
if(poolMode_ == PoolMode::MODE_CACHED)
{
//条件变量 超时返回了
if(std::cv_status::timeout == notEmpty_.wait_for(lock,std::chrono::seconds(1)))
{
auto now = std::chrono::high_resolution_clock().now();
auto dur = std::chrono::duration_cast<std::chrono::seconds>(now - lastTime);
if(dur.count() >= THREAD_MAX_IDLE_TIME
&& curThreadSize_ > initThreadSize_)
{
//开始回收当前线程
//记录线程数量相关变量值的修改
//把线程对象从线程容器列表中删除
//threadid -> thread对象 -> delete
threads_.erase(threadid); //std::this_thread::getid
curThreadSize_--;
idleThreadSize_--;
std::cout << "threadid:" << std::this_thread::get_id() << "exit!" << std::endl;
return;
}
}
}
else
{
//等待notEmpty条件
notEmpty_.wait(lock); //fix模式
}
//线程池结束 回收资源
if(!isPoolRunning_)
{
threads_.erase(threadid); //std::this_thread::getid
std::cout << "threadid:" << std::this_thread::get_id() << "exit!" << std::endl;
exitCond_.notify_all();
return;
}
}
idleThreadSize_--;
std::cout << "tid:" << std::this_thread::get_id()
<< "获取任务成功..." << std::endl;
//从任务队列中取一个任务出来
task = taskQue_.front();
taskQue_.pop();
taskSize_--;
//如果依然有剩余任务 继续通知其他的线程执行任务
if(taskQue_.size() > 0)
{
notEmpty_.notify_all();
}
//取出一个任务 进行通知 通知可以继续提交生产任务
notFull_.notify_all();
} //此时应该释放锁
//当前线程负责执行这个任务
if(task != nullptr)
{
// task->run(); //执行任务:把任务的返回值setVal方法给到Result
task->exec();
}
idleThreadSize_++;
lastTime = std::chrono::high_resolution_clock().now(); //更新线程执行完任务的时间
}
threads_.erase(threadid); //std::this_thread::getid
std::cout << "threadid:" << std::this_thread::get_id() << "exit!" << std::endl;
exitCond_.notify_all();
}
出现的问题及优化方式的思考
线程池析构时出现死锁
不可复现的问题出现往往会比较棘手,在线程池析构过程中出现了线程无法正常退出的问题
例如,启动了四个线程,但是main函数中pool析构的结果只显示了三个线程exit
以下是针对此问题的一些分析反思过程:
情况一:pool线程先抢到锁
pool线程抢到锁之后,执行exitCond_.wait,但此时线程函数停留在争夺taskQueMtx_,exitCond_.wait条件 threads_ == 0 无法得到满足,pool线程进入等待态,释放taskQueMtx_锁;与此同时task线程会开始获取互斥锁,获取到互斥锁之后,task线程进入notEmpty_.wait,释放互斥锁,进入等待态
综上 task 线程会因为无法得到 notify_all 而一直停滞在 notEmpty_wait(lock) 上造成死锁
情况二:线程池中的线程先抢到锁
当线程池中的线程先抢到锁时,ThreadFunc 会往下执行,直至停滞于 notEmpty_wait(lock),释放锁,线程转换成等待态;pool线程抢到互斥锁,但此时 threads_ == 0 并不满足,至此会造成线程死锁
解决方法:
改变线程池析构函数中 notEmpty_.notify_all()的位置,以及在ThreadFunc 中添加 锁 + 双重判断 的条件
void ThreadPool::ThreadFunc(int threadid) //线程函数返回,相应的线程也就结束了
{
auto lastTime = std::chrono::high_resolution_clock().now();
while(isPoolRunning_)
{
std::shared_ptr<Task> task;
{
//先获取锁
std::unique_lock<std::mutex> lock(taskQueMtx_);
std::cout << "tid:" <<std::this_thread::get_id()
<< "尝试获取新任务..." <<std::endl;
//cached模式下 可能已经创建了很多线程 但是空闲时间超过60s 应该把多余的线程回收掉
//结束回收掉 (超过initThreadSize_数量的线程要进行回收)
//当前时间 - 上一次线程执行的时间 > 60s
//每一秒钟返回一次 怎么区分:超时返回 or 有任务待执行返回
//锁 + 双重判断
while(isPoolRunning_ && taskQue_.size() == 0)
{
if(poolMode_ == PoolMode::MODE_CACHED)
{
//条件变量 超时返回了
if(std::cv_status::timeout == notEmpty_.wait_for(lock,std::chrono::seconds(1)))
{
auto now = std::chrono::high_resolution_clock().now();
auto dur = std::chrono::duration_cast<std::chrono::seconds>(now - lastTime);
if(dur.count() >= THREAD_MAX_IDLE_TIME
&& curThreadSize_ > initThreadSize_)
{
//开始回收当前线程
//记录线程数量相关变量值的修改
//把线程对象从线程容器列表中删除
//threadid -> thread对象 -> delete
threads_.erase(threadid); //std::this_thread::getid
curThreadSize_--;
idleThreadSize_--;
std::cout << "threadid:" << std::this_thread::get_id() << "exit!" << std::endl;
return;
}
}
}
else
{
//等待notEmpty条件
notEmpty_.wait(lock); //fix模式
}
//线程池结束 回收资源
// if(!isPoolRunning_)
// {
// threads_.erase(threadid); //std::this_thread::getid
// std::cout << "threadid:" << std::this_thread::get_id() << "exit!" << std::endl;
// exitCond_.notify_all();
// return;
// }
}
idleThreadSize_--;
if(!isPoolRunning_)
{
break;
}
std::cout << "tid:" << std::this_thread::get_id()
<< "获取任务成功..." << std::endl;
//从任务队列中取一个任务出来
task = taskQue_.front();
taskQue_.pop();
taskSize_--;
//如果依然有剩余任务 继续通知其他的线程执行任务
if(taskQue_.size() > 0)
{
notEmpty_.notify_all();
}
//取出一个任务 进行通知 通知可以继续提交生产任务
notFull_.notify_all();
} //此时应该释放锁
//当前线程负责执行这个任务
if(task != nullptr)
{
// task->run(); //执行任务:把任务的返回值setVal方法给到Result
task->exec();
}
idleThreadSize_++;
lastTime = std::chrono::high_resolution_clock().now(); //更新线程执行完任务的时间
}
threads_.erase(threadid); //std::this_thread::getid
std::cout << "threadid:" << std::this_thread::get_id() << "exit!" << std::endl;
exitCond_.notify_all();
}
测试结果:
多次测试避免了偶然性,结果表明 启动四条线程 均正常退出
资源回收问题
当pool线程只提交任务出作用域,不调用get方法阻塞,线程池结束仍然会有线程没有运行完;理想的结果线程池应该是等所有线程全部执行完再结束
解决方法:
改变循环条件以及线程回收的策略(针对ThreadFunc函数进行修改)
用 for(;😉 替换 while(isPoolRunning_),只要有任务就会先执行任务,再进行资源回收
void ThreadPool::ThreadFunc(int threadid) //线程函数返回,相应的线程也就结束了
{
auto lastTime = std::chrono::high_resolution_clock().now();
//所有任务必须执行完成,线程池才可以回收所有线程资源
for(;;)
{
std::shared_ptr<Task> task;
{
//先获取锁
std::unique_lock<std::mutex> lock(taskQueMtx_);
std::cout << "tid:" <<std::this_thread::get_id()
<< "尝试获取新任务..." <<std::endl;
//cached模式下 可能已经创建了很多线程 但是空闲时间超过60s 应该把多余的线程回收掉
//结束回收掉 (超过initThreadSize_数量的线程要进行回收)
//当前时间 - 上一次线程执行的时间 > 60s
//每一秒钟返回一次 怎么区分:超时返回 or 有任务待执行返回
//锁 + 双重判断
while(taskQue_.size() == 0)
{
//线程池结束 回收资源
if(!isPoolRunning_)
{
threads_.erase(threadid); //std::this_thread::getid
std::cout << "threadid:" << std::this_thread::get_id() << "exit!" << std::endl;
exitCond_.notify_all();
return;
}
if(poolMode_ == PoolMode::MODE_CACHED)
{
//条件变量 超时返回了
if(std::cv_status::timeout == notEmpty_.wait_for(lock,std::chrono::seconds(1)))
{
auto now = std::chrono::high_resolution_clock().now();
auto dur = std::chrono::duration_cast<std::chrono::seconds>(now - lastTime);
if(dur.count() >= THREAD_MAX_IDLE_TIME
&& curThreadSize_ > initThreadSize_)
{
//开始回收当前线程
//记录线程数量相关变量值的修改
//把线程对象从线程容器列表中删除
//threadid -> thread对象 -> delete
threads_.erase(threadid); //std::this_thread::getid
curThreadSize_--;
idleThreadSize_--;
std::cout << "threadid:" << std::this_thread::get_id() << "exit!" << std::endl;
return;
}
}
}
else
{
//等待notEmpty条件
notEmpty_.wait(lock); //fix模式
}
}
idleThreadSize_--;
std::cout << "tid:" << std::this_thread::get_id()
<< "获取任务成功..." << std::endl;
//从任务队列中取一个任务出来
task = taskQue_.front();
taskQue_.pop();
taskSize_--;
//如果依然有剩余任务 继续通知其他的线程执行任务
if(taskQue_.size() > 0)
{
notEmpty_.notify_all();
}
//取出一个任务 进行通知 通知可以继续提交生产任务
notFull_.notify_all();
} //此时应该释放锁
//当前线程负责执行这个任务
if(task != nullptr)
{
// task->run(); //执行任务:把任务的返回值setVal方法给到Result
task->exec();
}
idleThreadSize_++;
lastTime = std::chrono::high_resolution_clock().now(); //更新线程执行完任务的时间
}
}