模擬主執行緒等待子執行緒的過程

心媛意碼發表於2020-04-17

一.背景

今天看到一篇文章(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;
}

 

相關文章