偷工
线程池解决了多队列
线程池中"饥饿等待
"问题,即有的线程队列
中的任务
很多,有的线程队列
是空的
,处于"饥饿等待
"状态,偷工
可把其它
线程队列中的任务偷
过来避免"饥饿
",提高了线程池效率
.
偷工
线程池
实现思路
之前线程池都是通过阻塞
拿到队列中任务:
极 弹(T&项){
独锁 锁(_互斥锁);
_条件.等待(锁,[&](){中!_队列.空的()||_停止;});
如(_队列.空的())中 假;
项=移动(_队列.前());_队列.弹();
中 真;
}
条件变量
会一直阻塞
直到拿到任务
,能否先非阻塞
从其它队列
中取任务
呢?如果取不到
,则阻塞
取任务.
改进
有两个好处:
1,减少了阻塞
等待,因为它按非阻塞
方式从其它队列
中取任务的,无需阻塞;
2,实现了偷工
.
c++
标准库提供了按非阻塞
取锁适合偷工
的std::try_to_lock_t
.
接口设计
为了实现偷工
需要对队列
增加非阻塞
取任务接口:
bool try_pop(T&item);
为了保持灵活,在不需要偷工
时,关闭偷工
功能,因此还需要增加try_pop_if
来控制是否需要偷工
的接口:
bool try_pop_if(T& item, bool (*predict)(T&) = nullptr);
线程池
需要入队和出队
逻辑,入队时先试非阻塞
,失败再随机选某个线程
队列;出队
时先试从其它
线程队列中偷
一个,如果偷
成功就执行偷
来任务,失败就阻塞
等待取任务.
偷工核心代码:
内联 线程池::线程池(整32型 线程号,极 允许偷工)
:_线程号(线程号?线程号:线程::硬件并行()),
_队列(_线程号),
_允许偷工(允许偷工),
_停止(假){
动 工作者=[本](整32型 标识){
动 当前=取当前();当前->第一=标识;
当前->第二=本;
当(真){
工作项 工作者项={};
如(_允许偷工){
//先试偷
对(动 n=0;n<_线程号*2;++n){
如(_队列[(标识+n)%_线程号].试弹如(
工作者项,
[](动&项){中 项.可偷;}))
断;
}
}
//_enableWorkSteal为假,或偷失败,等待弹
//任务.
如(!工作者项.函数&&!_队列[标识].弹(工作者项)){
断;
}
如(工作者项.函数){
工作者项.函数();
}
}
};
_线程.保留(_线程号);
对(动 i=0;i<_线程号;++i){
_线程.原后(工作者,i);
}
}
内联 线程池::错误类型 线程池::按标识调度(函数<空()>函数,
整32型 标识){
如(空针==函数){
中 是无效错误项池;
}
如(_停止){
中 有停止池错误;
}
如(标识==-1){
如(_允许偷工){
//先压非阻塞队列
工作项 工作者项{/*可偷=*/真,函数};
对(动 n=0;n<_线程号*2;++n){
如(_队列.在(n%_线程号).试压(工作者项))
中 无错;
}
}
标识=随机()%_线程号;
_队列[标识].压(
工作项{/*可偷=*/_允许偷工,移动(函数)});
}
异{
断定(标识<_线程号);
_队列[标识].压(工作项{/*可偷=*/假,移动(函数)});
}
中 无错;
}
测试代码:
用 名字空间 简单异步::工具;
常 整 计数=500'000;
常 整 重复=10;
空 自动调度任务(极 允许偷工){
原子<整>数=0;
{
线程池 tp(线程::硬件并行(),允许偷工);
对(整 i=0;i<计数;++i){
[[也许未用]]动 中=tp.按标识调度([i,&数]{
数++;整 x;
动 重复=重复+(重复*(随机()%5));
对(整 n=0;n<重复;++n)
x=i+随机();
(空)x;
});
断定(中==线程池::错误类型::无错);
}
}
断定(数==计数);
}
空 线程池无偷工(基准::状态&状态){
对([[也许未用]]常 动&_:状态)
自动调度任务(/*允许偷工=*/假);
}
空 线程池有偷工(基准::状态&状态){
对([[也许未用]]常 动&_:状态)
自动调度任务(/*允许偷工=*/真);
}