在上一篇 【线程池项目(三)】线程池CACHED模式的实现中我们大概说了说cached
模式的基本实现,对于多线程编程,我们需要考虑的问题也较于单线程更多、更复杂,经常存在线程同步、资源竞争等复杂的并发控制问题,因此上篇的文末留下了三个问题🤔🤔🤔
这是本篇博客关注的第一个死锁问题,另一个就是在跨平台编译(Ubuntu20.04)时,由于平台差异化而引起的出现的死锁性问题,下面我们来详细的剖析一下
如果需要与本篇博客完全匹配的实现代码,也可以在 我的gitee 上下载对应的源码
项目经历——基于C++新特性以及模板编程实现的线程池
一、三种状态的可能性死锁问题🧐🧐🧐
我们先来看看前两种情况
-
🤔 当线程数多余任务队列中的任务数或者是任务队列为空时,在一定的时间内,肯定会有某个或者某些线程处于空闲的
wait
状态等待任务的到来,如果这时用户准备结束主程序(即执行线程池的析构函数),将isPoolRunning置为false,并进行notEmpty.notify_all()通知,等待在wait
上的线程被唤醒往下执行,在下面的if判断语句中满足结束条件,正常的回收线程;cached模式通过自己的回收机制也能正常的回收线程,不会造成死锁问题的发生 -
🤔 当某个线程正在执行任务
exec
且还未执行完时,用户准备结束主程序(即执行线程池的析构函数),析构函数中的语句对正在执行任务的线程没有任何影响,当线程执行任务完成且更新完任务执行完的时间后,同样也能够通过while()的判断条件正常的退出,进而被回收,也不会发生死锁问题
这是第三种情况
- 🤔 当某个线程执行任务完成,且用户准备结束主程序(即调用线程池的析构函数)时,线程在isPoolRunning被主程序置为false之前,执行完任务的线程已经进入了外边的while循环,并且还没有执行到
fixed
模式下的wait
,这时主线程往下执行析构函数,通知等待的线程,并等待所有线程被回收,之后执行完任务的线程再往下执行至notEmpyt.wait()
,显然现在线程池中只有一个线程等待被唤醒,而用户所在的主线程也阻塞在exitCond_wait()
上,出现了死锁现象
🫠以下是正常结束的执行结果(具有偶然性)
🫠这是发生死锁问题的执行结果,有一个任务线程没有被回收,主程序线程也被无限期等待
二、对应的解决方案😮😮😮
来看看修改后的代码
- 当主线程执行析构函数时先抢到锁,线程池中的线程会在定义
unique_lock
上阻塞,进而主线程继续执行通知正在wait
的线程,而无论是正在执行任务的线程,还是wait
状态的线程,都不会出现死锁问题,因为执行任务的线程不影响,而wait
状态的线程会在被唤醒后,也会在if条件下break
,正常的被回收- 当线程池里面的线程先抢到锁时,主线程线程会阻塞在
unique_lock
上,线程池里的线程往下执行,无论是执行到wait
语句,还是直接获取到任务exec
,主程序线程总是会比线程池里的线程慢一步,后执行notify_all()
操作来唤醒等待的线程,因此也不会发生死锁问题
三、资源回收机制的改良😦😦😦
前面我们对于线程池的析构函数进行了优化,完善了线程池的资源回收机制,但还是有问题没考虑到,我们资源回收关注的点仅仅是线程,并没有过多的看到任务的具体执行情况,即当线程池要析构、结束时,如果还有用户提交的任务没有执行完,该怎么办 ? 当然,我们很容易想到,既然线程池都已经结束了,那里边正在执行的任务也势必会直接结束。但这仅仅是对我们自己而言,要实现一个完整、通用的系统,我们肯定是需要等待任务执行完毕才行,我们更改以下测试程序,看看会发生什么
下面是执行结果
看看,用户如果不需要获取任务执行完毕的返回值(即调用get()
方法阻塞),只是让线程池执行任务,他辛辛苦苦提交的任务根本就没有被执行,然而主线程就直接退出了!!!因此我们仍然需要修改我们之前优化过的代码!!!
图例不太清除,这里就不介绍了,可以在 我的gitee 上下载对应的源码进行对比分析
ubuntu20.04编译动态库
g++ threadpool.cpp -fPIC -shared libpool.so -std=c++17
动态库搜索路径/usr/local/lib
头文件搜索路径usr/local/include
测试代码执行命令g++ main.cpp -o a -lpool -lpthread -std=c++17
四、平台差异化的死锁性问题(没有发生,代码竟然能跑🤣🤣)
Visual Stdio 2022 的 condition_variable 底层源码,完善了条件变量的析构函数,正常释放
Linux系统上 condition_variable 的底层源码,析构函数没有做任何操作
实际上,我在Linux上运行该项目时,并没有遇到老师提到的这些情况😵😵,这可能是因为我的配置存在问题,或者是因为在后续版本中开发者大佬们解决了这些问题。
五、对应的解决方案🥹🥹🥹
使用Linux环境下的gdb调试,通过
gdb attach PID
,调试当前正在运行的程序,ps -u
可以查看当前正在执行的进程任务,info threads
查看所有线程的状态以及当前正在哪个关注线程,thread n
关注第n个线程,bt
打印该线程执行函数的堆栈信息,进行问题分析。
六、总结😩😩😩
以上内容涉及了《线程池项目(四)》项目中死锁问题的分析以及资源回收机制的改进。关于项目后续采用packaged_task和future进行的二次开发和重构的详情就不介绍了,其源码可在 我的gitee 上找到。这也是本系列《手写线程池》的最后一篇🔚 。未来,我将与大家分享更多项目。我知识有限,博客中还有很多地方需要改进,出现问题希望大佬们能够及时指正,感谢大家的支持!🌻🌻🌻