子线程返回方式矛盾
williamWANG0325 opened this issue · comments
子线程工作函数如下
while (_run) /****** 1/
{
Task task; // 获取一个待执行的 task
{
// unique_lock 相比 lock_guard 的好处是:可以随时 unlock() 和 lock()
unique_lock<mutex> lock{ _lock };
_task_cv.wait(lock, [this]{
return !_run || !_tasks.empty();
}); // wait 直到有 task
if (!_run && _tasks.empty()) /****** 2/
return;
task = move(_tasks.front()); // 按先进先出从队列取一个 task
_tasks.pop();
}
_idlThrNum--;
task();//执行任务
_idlThrNum++;
}
由上述代码设计可见,子线程结束的方式有两种(代码中 /******/处):
- 第一处,当_run为false,即线程池析构时立刻停止。(这种方式结果为,当线程池析构时立刻回收所有子进程,丢弃任务队列中所有未完成任务)
- 第二处,要求_run为false且任务队列为空。(这种方式结果为,当线程池析构时,先完成剩下的所有任务再结束,个人认为这种设计更符合直觉)
两种方式混合使会用导致:当线程池析构时,如果任务队列还有任务,那么所有子线程会被唤醒,执行一次任务后被回收。既没有立即回收线程,也没有完全清空任务队列,就感觉挺迷惑的,如果要修改,应该使得子线程返回方式统一
- 要么将/****** 2/处的
&&_tasks.empty()
去掉,这样也就符合上面所说的第一种情况 - 要么将/****** 1/处的
while (_run)
改为while(true)
,这样也就符合上面所说的第二种情况
其实我认为作者是想写出第二种情况的代码的。如果这样的话/****** 1/处可能是个小bug,这会导致线程池在析构时可能会出错。
下面有个小例子可以证明我的想法。这是一个非常简单的多线程累加器,当却会导致未定义行为。
注意,该代码我在我自己的mac上用clang编译是没有问题的。但在centos上用g++编译就会出现问题,输出的结果是0,原因是线程池中的4个子线程在执行while (_run)
执行前,线程池就进行析构(将_run修改为false),导致子线程直接返回,抛弃任务队列中的所有的任务,导致结果为0。(这里有个有意思的现象,我在centos上使用gdb调试来运行,结果又是正确的)可以试着在服务器上运行一下这个代码,看会不会出现相同的情况,我的环境是:CentOS Linux release 7.9.2009 (Core)
、 g++ (GCC) 4.8.5 20150623 (Red Hat 4.8.5-44)
#include "ThreadPool.h"
int x = 0;
std::mutex mu;
void fun() {
std::unique_lock<std::mutex> m{mu};
for(int i = 0; i < 10000; i++)
x++;
}
int main() {
{
std::threadpool tp;
for(int i = 0; i < 100; i++)
tp.commit(fun);
}//该作用域为了析构tp,确保线程池中所有线程已经运行完
printf("%d\n", x); // 结果应该为 1000000
}
其实类似上面这种情况发生的概率其还挺大,只要当线程池析构的时候,任务队列中还有任务就会出现这种情况,最后会导致这种迷惑行为。
其实我猜想,作者设计那个while(_run)
的判定可能是为了让线程池析构的时候,commit
函数无法再创建新的子进程,但我觉得就算创建了其实也问题不大,反正可以在后面返回,不会产生不好的结果。
综上所述,我建议把第90行改成while(true)
。
(不知道我分析的对不对...,或者作者这样设计有别的用意?)
谢谢, while(true)
是有必要 commit 2e0c5e8