一、固定线程池——FixedThreadPool
(1)没有输出func,且显示核心转储
线程池对象被销毁时,池子里的 std::thread 线程对象还处于 joinable() 状态(既没 join() 也没 detach()),C++ 标准强制调用 std::terminate() 终止程序。
日志里出现了 func,说明线程池的任务函数执行了,但线程没被正确回收。
失败原因:将任务添加到线程池,因为主线程的运行速度太快,连执行的机会都没有就结束了
改进:做了休眠,等待任务执行
gdb调试:
0 __GI_raise → 发出 SIGABRT 信号
1 __GI_abort → 终止程序
2 std::terminate() → C++ 强制终止(因为线程没 join)
3 std::thread::~thread() → 线程对象析构时发现还没 join ...
16 std::list::clear() → 你的线程组被清空了
17 pool::FixedThreadPool::StopThreadGroup() → 你的 Stop 函数
21 call_once → 确保 StopThreadGroup() 只执行一次
27 Stop() → 析构里调用 Stop()
28 ~FixedThreadPool() → 线程池析构函数
31 main() → 程序入口
pool::FixedThreadPool::StopThreadGroup() 就是StopThreadGroup 函数!
崩溃的直接原因就是:在 StopThreadGroup 里调用了 list::clear(),而此时线程还没被 join。
std::this_thread::yield():主线程让出CPU会导致偶尔打印不出来
问题根源:yield 不是用来 “等待” 的
std::this_thread::yield()的作用是让出当前的 CPU 时间片,它只会让主线程 “暂停一小会儿”,但不会等待任何条件完成。
在这个场景里:
- 主线程调用
yield(),让出 CPU - 调度器可能马上又把 CPU 分给主线程,也可能分给工作线程
- 如果调度器又给了主线程,主线程会立刻继续执行
return 0,程序直接退出 - 工作线程还没来得及执行
cout,程序就结束了,所以打印不出来
yield()是 “让一下”,不是 “等一下”,所以它的等待效果是不可靠的、随机的,这就是你偶尔能打印、偶尔不行的原因。
ac:9999先打印出来了- 但最后一个任务
func:9999才打印出来 - 说明主线程提前执行了
cout << "ac:",但此时最后一个任务还没执行完
(2)线程池的拒绝策略
线程池满了,自己处理任务
(3)任务是无参无返回值的,如何返回其他类型
将任务加入到任务队列:
报错原因:因为std::function是可以拷贝的,所以要求内部包含的所有东西都是可以拷贝的,但是
std::packaged_task由于限制没有拷贝和赋值,支持移动,所以std::function中不能直接放std::packaged_tast,因此:
packaged_task放在堆上- 只创建一份
- 用
shared_ptr管理它 shared_ptr是可以随便拷贝的
成功获取返回值类型:
(4)#include<latch>一直标红,版本是C++20也标红
还不知道为什么?
(5)CPU利用率和运行效率对比
两核:
不使用线程池:一个CPU的利用率为100%
使用线程池:两个CPU的利用率为100%
轻任务:单线程 > 线程池 生成随机数、简单计算→ 线程池调度开销太大,反而慢
重任务:线程池 >>> 单线程 排序、复杂计算、IO→ 线程池多核并行,快几倍甚至十几倍
16核:
核数越多效率越快!
(6)为什么使用多线程申请内存时,比单线程申请内存的速度还要慢,慢在什么地方,为什么慢?提前预留空间也并没有很大优化?
内存分配是天生的 “串行” 操作,多线程争抢只会让它变慢。
核心原因:内存分配器(malloc)是 “带锁的”
我们平时用的new/malloc,底层都是调用操作系统的内存分配器,最常见的是glibc的ptmalloc。为了保证线程安全,它内部有一把全局锁。
单线程场景:
线程直接调用malloc,没有任何竞争,也没有加锁、解锁的开销。
分配器可以高效地找到合适的内存块,直接返回。
多线程场景:
多个线程同时调用malloc,都要争抢这把锁。
同一时间,只有一个线程能拿到锁进行分配,其他线程都必须阻塞等待。
这种场景下,多线程分配就变成了串行排队执行,而且额外增加了大量的锁竞争、线程上下文切换开销。
提前预留空间(reserve)为什么优化不大?
提前 reserve(m) 空间,只是避免了内存拷贝,但绕不开 malloc 本身的瓶颈。 reserve 只是一次性申请一块大内存,避免了多次扩容的拷贝开销。 但在多线程下,每个 vector 调用 reserve 时,依然要去争抢全局锁,所以锁竞争的问题还是存在。 当你一次性 reserve 大块内存时,锁持有时间反而变长了,其他线程等待的时间也更长了,甚至可能比动态扩容更慢。
(7)优化:
resize:创建并初始化
- 把
size设为 m - 创建 m 个元素
[]访问100% 安全
单线程和多线程同时用了resize预留空间但是使用的是自定义的随机生成函数且多线程用了门栓,时间对比如下:
单线程和多线程同时用了resize预留空间以及#include<random>中自带的随机生成函数且多线程用了门栓,时间对比如下:
reserve:看到的时间不准、是假的、是非法访问导致的!
- 开辟m 个空间
- size 依然是 0
- 不创建任何元素
正确编写后:
resize比reserve更快速
单线程和多线程同时用了reserve预留空间但是使用的是自定义的随机生成函数且多线程用了门栓,时间对比如下:
单线程和多线程同时用了reserve预留空间以及#include<random>中自带的随机生成函数且多线程用了门栓,时间对比如下:
发现造成多线程生成数据耗时如此慢的主要原因是因为随机数生成,换成系统的随机函数,更安全更高效
(8)wait和wait_for
(9)C++ 线程池的析构顺序与生命周期
二、缓存线程池——CachedTreadPool
CPU占比
不使用线程池时,CPU占比:CPU1处于100%
使用缓存式线程池时,CPU占比:
最初:提前用resize开辟了空间,用了自定义的随机生成函数
单线程和多线程同时提前用resize开辟了空间,用了自定义的随机生成函数且多线程用了门栓,时间对比如下:
第一次优化:提前用rerserve开辟空间,并且用了random库的随机生成
单线程和多线程同时提前用reserve开辟了空间,并且多线程用了random库的随机生成数和门栓,时间对比如下:
第二次优化:提前用resize开辟空间,并且用了random库的随机生成
单线程和多线程同时提前用resize开辟了空间,并且多线程用了random库的随机生成数和门栓,时间对比如下:
三、工作窃取线程池——WorkStealingPool
(1)出现了死锁
是因为在获取任务时使用了wait,导致了死锁
(2)出现了无输出的情况
Debug:
cmake -DCMAKE_BUILD_TYPE=Debug ..
make -j$(nproc)
启动调试:
gdb ./threadpool3
运行调试:
run
发现卡住不动:
中断程序:ctrl+c
查看所有线程:
info threads
查看函数掉用栈:
thread 1
bt
#6 main()程序从main函数开始运行。
#5 test2() at threadpoolTest3.cpp:97进入了test2()函数,运行到第 97 行。
#4 std::latch::wait()主线程在这里卡住了!它在调用:lat_steal.wait();
#3 ~ #0 底层系统等待主线程已经被操作系统挂起,不占 CPU,不执行任何代码,死等。
主线程在 test2 函数的第 97 行,也就是 lat_steal.wait() 处,永远等不到任务完成。
再查看一个非主线程的运行状态:
#0 futex_abstimed_wait_cancelable
#1 pthread_cond_wait_common
#2 pthread_cond_clockwait
#3 std::condition_variable::wait_until
#4 std::condition_variable::wait_for
#5 SyncQueue::Task() at SyncQueue_4.hpp:98
#6 WorkStealingPool::RunThread() at WorkStealingPool.hpp:50
- 主线程卡死在
latch.wait()→ 任务没执行完 - 工作线程全部卡在
SyncQueue::Task()→ 拿不到任务 - 提交任务时用了默认队列容量 500,而任务数是 10000 → 队列早就塞满了,主线程在
Put()时就已经阻塞,后面的任务根本没提交进去 - 队列里的任务数为 0 → 工作线程永远拿不到任务,只能一直
wait_for循环
mypool() 队列太小,装不下 10000 个任务 → 程序堵死卡死 → 无输出
mypool(10000,16) 队列够大 → 能装下所有任务 → 正常跑 → 有输出
单线程CPU利用率:
多线程CPU利用率:
最初:提前用resize开辟了空间,用了自定义的随机生成函数
单线程和多线程同时提前用resize开辟了空间,用了自定义的随机生成函数且多线程用了门栓,时间对比如下:
第一次优化:提前用rerserve开辟空间,并且用了random库的随机生成
单线程和多线程同时提前用reserve开辟了空间,并且多线程用了random库的随机生成数和门栓,时间对比如下:
第二次优化:提前用resize开辟空间,并且用了random库的随机生成
单线程和多线程同时提前用resize开辟了空间,并且多线程用了random库的随机生成数和门栓,时间对比如下:
四、ScheduleThreadPoo——定时线程池
(1)定时器出现了周期延迟
TimerManage 的 loop 线程
↓
epoll_wait
↓
触发定时器 A → 执行回调
↓
触发定时器 B → 执行回调
如果 func() 执行很慢,后面的定时器,必须等它执行完 ,周期就会越来越不准、越来越延迟
定时器线程不阻塞:
Heavy task执行期间,Fast task仍然每秒准时执行任务并发执行:
Fast task1和Fast task 2几乎同时执行(时间差仅1ms)周期性任务不受影响:所有定时器都按预期间隔触发
(2)使用map,实现1个Timer管理N个Callback
时间精度: