Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

子线程返回方式矛盾 #17

Closed
williamWANG0325 opened this issue Feb 12, 2022 · 1 comment
Closed

子线程返回方式矛盾 #17

williamWANG0325 opened this issue Feb 12, 2022 · 1 comment

Comments

@williamWANG0325
Copy link

子线程工作函数如下

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++;
}

由上述代码设计可见,子线程结束的方式有两种(代码中 /******/处):

  1. 第一处,当_run为false,即线程池析构时立刻停止。(这种方式结果为,当线程池析构时立刻回收所有子进程,丢弃任务队列中所有未完成任务)
  2. 第二处,要求_run为false且任务队列为空。(这种方式结果为,当线程池析构时,先完成剩下的所有任务再结束,个人认为这种设计更符合直觉)

两种方式混合使会用导致:当线程池析构时,如果任务队列还有任务,那么所有子线程会被唤醒,执行一次任务后被回收。既没有立即回收线程,也没有完全清空任务队列,就感觉挺迷惑的,如果要修改,应该使得子线程返回方式统一

  1. 要么将/****** 2/处的&&_tasks.empty()去掉,这样也就符合上面所说的第一种情况
  2. 要么将/****** 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
}

运行结果:
image

其实类似上面这种情况发生的概率其还挺大,只要当线程池析构的时候,任务队列中还有任务就会出现这种情况,最后会导致这种迷惑行为。
其实我猜想,作者设计那个while(_run)的判定可能是为了让线程池析构的时候,commit函数无法再创建新的子进程,但我觉得就算创建了其实也问题不大,反正可以在后面返回,不会产生不好的结果。

综上所述,我建议把第90行改成while(true)

(不知道我分析的对不对...,或者作者这样设计有别的用意?)

@lzpong
Copy link
Owner

lzpong commented Jun 2, 2022

谢谢, while(true) 是有必要 commit 2e0c5e8

@lzpong lzpong closed this as completed Jun 2, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants