模拟主线程等待子线程的过程
一.背景
今天看到一篇文章(https://www.xuebuyuan.com/1691810.html),讲到主线程等待子线程执行完毕,如何判断的问题.作者一开始采用的是简单的Sleep操作,但这种操作不能够保证正确而且效率还比较低.
于是作者自己设计了判断子线程结束的方法,我也就实践了这个方法.(其实Windows中提供的有系统函数来满足需求,它就是WaitForMultipleObjects,不过自己设计还是更有成就感的)
二.失败的尝试
在设计子线程时,我考虑可以采用采用随机数的方式来模拟不同线程处理时间不同的情况;而子线程中参数是把主线程循环的下标i传进来,考虑这样应该可以满足需求.最后结果让人吃惊.
发现结果竟然打印的都是第20个线程.按理来说i是从0到19的.出现这种情况的原因是主线程中把所有的子线程都创建完后,才去执行子线程中的内容,此时i的值都变成了20了.
我这个时候,想到了使用同步的方式来解决,比如在子线程和主线程中加上临界区,后来发现没有用.因为在CreateThread的循环执行完,子线程才开始运行,因此根本就不存在同时访问的事情,所以同步谈不上.
看来这种方法是行不通的,从设计上来说一开始就存在问题吧.
(1)多线程共享的变量要选择全局变量
(2)CreateThread后面没有sleep,会导致子线程创建完后没有及时启动.
有一种比较蹩脚的改法,现有代码不动,在CreateThread后面添加Sleep(2000),这个实际上就是坏味道了.
#include <iostream> #include "windows.h" #include "stdlib.h" using namespace std; #define MAX 2000 #define MIN 0 #define THREADNUM 20 int g_nFlag[THREADNUM] = { 0 }; DWORD WINAPI ThreadFun(void * param) { int *pi = (int*)param; srand(unsigned(*pi)); int n = MIN + rand()%(MAX-MIN+1);//产生MIN到MAX之间的随机整数. //通过休眠随机的时间来模拟不同线程处理时间的长短 Sleep(n); cout << "第" << *pi << "个线程,休眠时间为" << n << "ms\n"; g_nFlag[*pi] = 1; return 0; } int main() { //创建子线程 HANDLE h[THREADNUM]; DWORD ThreadID[THREADNUM]; for (int i = 0; i < THREADNUM; i++) { h[i] = CreateThread(0, 0, ThreadFun, &i, 0, &ThreadID[i]);//不能返回局部变量的引用和指针,但是传递有没有问题呢? //应该是没有问题的,因为传递完之后,局部变量还在生命周期,比如这里的i; //而返回局部变量的引用和指针,则是被调函数已经结束了,里面的堆栈都被销毁了. //Sleep(2000); //问题:子线程当前过程(i=0)还没有执行到g_nFlag[*pi] = 1,主线程就把i修改成了1,导致i=0被跳过了. } //等待线程全部结束. //方法2:自己创造方法实现 while (1) { for (int i = 0; i < THREADNUM; i++) { //如果某个线程还没有结束,那么让主线程等待100ms,然后从头再去判断每个线程,这个for循环结束的条件是数组中所有元素都为1. if (g_nFlag[i] == 0) { Sleep(100); cout << "当前的i:" << i << endl; i = -1; // } } cout << "子线程已完全结束掉!" << endl; break; } return 0; }
运行结果.
第20个线程,休眠时间为45第20个线程,休眠时间为51ms
第20个线程,休眠时间为54ms
第20个线程,休眠时间为64ms
第20个线程,休眠时间为51ms
ms
当前的i:0
第20个线程,休眠时间为45ms
第20个线程,休眠时间为64ms
第20个线程,休眠时间为103ms
第20个线程,休眠时间为103ms
第20个线程,休眠时间为103ms
第20个线程,休眠时间为103ms
第20个线程,休眠时间为103ms
三.持续重构
既然之前的设计就不够合理,那就不在上面缝缝补补了,换一种相对正确的方式来做.
去在一开始定义一个全局变量g_nThreadNo,在子线程中进行自增,这样就不会出现和主线程扯不清的联系了.
#include "windows.h" #include "stdlib.h" using namespace std; #define MAX 2000 #define MIN 0 #define THREADNUM 20 int g_nFlag[THREADNUM] = { 0 }; int g_nThreadNo = 0; CRITICAL_SECTION g_SC;
DWORD WINAPI ThreadFun(void * param) { EnterCriticalSection(&g_SC); srand(unsigned(g_nThreadNo)); //一开始使用的unsigned(time(0)),发现子线程几乎同时开始的,所以rand的种子是一样的 //后来改成unsigned(g_nThreadNo),但是放在EnterCriticalSection前面,发现也是多个线程同时访问,导致g_nThreadNo是同一个. int n = MIN + rand() % (MAX - MIN + 1);//产生MIN到MAX之间的随机整数. //通过休眠随机的时间来模拟不同线程处理时间的长短 Sleep(n); cout << "第" << g_nThreadNo << "个线程,休眠时间为" << n << "ms\n"; g_nFlag[g_nThreadNo] = 1; g_nThreadNo++; LeaveCriticalSection(&g_SC); return 0; } int main() { InitializeCriticalSection(&g_SC); //创建子线程 HANDLE h[THREADNUM]; DWORD ThreadID[THREADNUM]; for (int i = 0; i < THREADNUM; i++) { h[i] = CreateThread(0, 0, ThreadFun, NULL, 0, &ThreadID[i]); } //方法2:自己创造方法实现 while (1) { for (int i = 0; i < THREADNUM; i++) { //如果某个线程还没有结束,那么让主线程等待100ms,然后从头再去判断每个线程,这个for循环结束的条件是数组中所有元素都为1. if (g_nFlag[i] == 0) { Sleep(100); cout << "当前的i:" << i << endl; i = -1; // } } cout << "子线程已完全结束掉!" << endl; break; } DeleteCriticalSection(&g_SC); return 0; }
这个时候的打印是这样的:
第0个线程,休眠时间为38ms 第1个线程,休眠时间为41ms 当前的i:0 第2个线程,休眠时间为45ms 第3个线程,休眠时间为48ms 当前的i:2 第4个线程,休眠时间为51ms 第5个线程,休眠时间为54ms 当前的i:4 第6个线程,休眠时间为58ms 当前的i:6 第7个线程,休眠时间为61ms 第8个线程,休眠时间为64ms 当前的i:7 第9个线程,休眠时间为68ms 当前的i:9 第10个线程,休眠时间为71ms 第11个线程,休眠时间为74ms 当前的i:10 第12个线程,休眠时间为77ms 当前的i:12 第13个线程,休眠时间为81ms 当前的i:13 第14个线程,休眠时间为84ms 当前的i:14 第15个线程,休眠时间为87ms 当前的i:15 第16个线程,休眠时间为90ms 当前的i:16 第17个线程,休眠时间为94ms 当前的i:17 第18个线程,休眠时间为97ms 当前的i:18 第19个线程,休眠时间为100ms 当前的i:19 子线程已完全结束掉!
这里要注意:
对子线程中的g_nThreadNo进行同步操作,不然他们会同时访问g_nThreadNo,造成意想不到的问题.
比如当时产生的结果是这样的:
打印出来的文字是交叉的;打印都是0线程,但最后g_nFlag[g_nThreadNo] = 1的操作却是正确的.
单步调了下,发现原因是子线程先都执行了cout那一行,然后后面的赋值和自增部分又能够交替进行了.
第0个线程,休眠时间为38第0第0第0个线程,休眠时间为38ms
第0个线程,休眠时间为38ms
第0个线程,休眠时间为38ms
第0第0第0第0个线程,休眠时间为38ms
第0个线程,休眠时间为38ms
第0个线程,休眠时间为38ms
个线程,休眠时间为38ms
个线程,休眠时间为38第0个线程,休眠时间为38ms
第0个线程,休眠时间为38ms
第0个线程,休眠时间为38ms
ms
第0个线程,休眠时间为38ms
ms
个线程,休眠时间为38ms
第0个线程,休眠时间为38ms
个线程,休眠时间为38ms
第0个线程,休眠时间为38ms
第0个线程,休眠时间为38ms
第0个线程,休眠时间为38ms
个线程,休眠时间为38ms
当前的i:0
子线程已完全结束掉!
请按任意键继续. . .
ps:
采用windows自带的系统函数,用起来就很开心了.
CRITICAL_SECTION g_SC; DWORD WINAPI ThreadFun(void * param) { EnterCriticalSection(&g_SC); srand(unsigned(g_nThreadNo)); //一开始使用的unsigned(time(0)),发现子线程几乎同时开始的,所以rand的种子是一样的 //后来改成unsigned(g_nThreadNo),但是放在EnterCriticalSection前面,发现也是多个线程同时访问,导致g_nThreadNo是同一个. int n = MIN + rand() % (MAX - MIN + 1);//产生MIN到MAX之间的随机整数. //通过休眠随机的时间来模拟不同线程处理时间的长短 Sleep(n); cout << "第" << g_nThreadNo << "个线程,休眠时间为" << n << "ms\n"; g_nThreadNo++; LeaveCriticalSection(&g_SC); return 0; } int main() { InitializeCriticalSection(&g_SC); //创建子线程 HANDLE h[THREADNUM]; DWORD ThreadID[THREADNUM]; for (int i = 0; i < THREADNUM; i++) { h[i] = CreateThread(0, 0, ThreadFun, NULL, 0, &ThreadID[i]); } //等待线程全部结束. //方法1:使用系统提供的方法 WaitForMultipleObjects(THREADNUM, h, TRUE, INFINITE); DeleteCriticalSection(&g_SC); return 0; }