1. 程式人生 > >Visual C++利用互斥量同步執行緒實現檔案讀取進度條

Visual C++利用互斥量同步執行緒實現檔案讀取進度條

忘了原文的位置了。

一、前言

        檔案讀取進度條的實現可以有很多種方法,常用的是在讀取檔案的過程中隔一定時間向對話方塊傳送訊息以控制進度條的位置,但是這種方法很難確定隔多少時問傳送一個訊息,因為檔案的大小是不確定的,時間間隔長了可能檔案已經讀取完了還沒有傳送訊息,而訊息傳送得太頻繁又會影響檔案讀取的效率。特別是在讀取文字檔案時你可能需要在每一個ReadString()函式之後都要傳送一個訊息,而在一些格式比較複雜的檔案讀寫程式碼中(例如dxf檔案的讀取),這樣的讀取函式迴圈可能有幾十處,在這樣的程式碼中傳送訊息是很繁瑣的事情。而利用執行緒同步則可以很好地解決這個問題。         程序是一個可執行的程式,由私有虛擬地址空間、程式碼、資料和其他作業系統資源(如程序建立的檔案、管道、同步物件等)組成。一個應用程式可以有一個或多個程序,一個程序可以有一個或多個執行緒,其中一個是主執行緒。         執行緒是作業系統分時排程分配CPU時問的基本實體。一個執行緒可以執行程式的任意部分的程式碼,即使這部分程式碼被另一個執行緒併發地執行,一個程序的所有執行緒共享它的虛擬地址空間、全域性變數和作業系統資源。         建立一個新的程序必須載入程式碼,而執行緒要執行的程式碼已經被對映到程序的地址空間,所以建立、執行執行緒的速度比程序更快。另外,一個程序的所有執行緒共享程序的地址空間和全域性變數簡化了執行緒之間的通訊,所以以執行緒為排程物件要比以程序為排程物件效率高。但是在幾個執行緒並行執行時,可能會存線上程的同步問題。例如:兩個執行緒同時對一個全域性陣列進行操作,執行緒A取得對該陣列的控制權對陣列進行寫入,當寫入還未完成時,控制權又由執行緒B取得,執行緒B 改變了該陣列的資料,然後執行緒A又取得控制權進行讀取,這樣執行緒A獲取的資料可能並不足其所需要的資料,這時就要用執行緒同步來解決這個問題。         Windows提供了幾種同步物件來實現執行緒的同步,常用的有臨界區(critical section)、互斥量(mutexe)、訊號量(semaphore)、事件(event)和可等待的記時器(waitable timer)等。執行緒主要使用兩個函式將它們設為睡眠來等待核心物件變為有訊號:  


DWORD WaitForSingleObject(HANDLE hObject, DWORD dwTimeOut)
DWORD WaitForMultipleOblects(DWORD cObject, LPHANDLE lpHandles)
        函式WaitForSingleObject告訴系統執行緒在等待由引數hObject標識的核心物件變為有訊號。引數dwTimeOut 告訴系統執行緒願意等待多少毫秒。如果指定的核心物件在指定時間內沒有變為有訊號,系統就會喚醒執行緒,讓它繼續執行。函式的返回值有3 種:WAIT_OBJECT_0表示物件達到有訊號狀態;WAIT_TIMEOUT表示物件在dwTimeOut毫秒內未達到有訊號狀態;WAIT_ABANDONED表示物件是一個互斥量,由於被放棄而達到了有訊號的狀態。  


二、實現方法
        首先我們宣告一個全域性的檔案指標g_pFile以存放要讀取的檔案指標,然後在主執行緒中指定要讀取的檔案,初始化g_pFile,並進行進度條對話方塊的建立。然後開啟兩個執行緒ReadDxfThreadProc(LPVOID lParam)和SetProgressPosThreadProc(LPVOID lParam),第一個執行緒用來讀取檔案,第二個執行緒則通過g_pFile指標來判斷當前檔案指標的位置,並向對話方塊傳送訊息以控制進度條的位置。這樣我們將檔案讀取和向對話方塊傳送訊息分離,就不必在每個檔案讀取語句後都發送訊息了,而且檔案讀取的效率也較高:         在編碼過程中有以下幾個問題要注意:         (l)在建立兩個執行緒的時候必須將它們的優先順序設定為同級,否則會造成一個執行緒已經結束了而另一個執行緒還沒有開始。         (2)由於兩個執行緒都要用到g_pFile指標,因此需要對兩個執行緒進行同步,否則會出現異常。在本例項中我們利用互斥量來對兩個執行緒進行同步。在宣告全域性檔案指標的同時我們也宣告一個互斥量控制代碼:HANDLE hMutex = NULL,並在主程序開啟執行緒之前建立互斥量,對g_hMutex進行初始化:g_hMutex = CreateMutex(NULL, FALSE, NUlL);         (3)由於本例項中進度條對話方塊使用的是非模態劃話框,因此除了需要定義設定進度條位置的訊息響應函式之外,還需要定義銷燬對話方塊的訊息響應函式。         (4)線上程SetProgressPosThreadProc中我們通過全域性變數g_pFile來探測檔案讀寫的進度。當檔案讀寫完畢以後我們需要對非模態的進度條對話方塊傳送一個銷燬對話方塊窗的訊息以關閉對話方塊。         (5)當檔案讀取完畢以後,我們需要將全域性的檔案指標g_pFile銷燬並置空。由於在 SetProgressPosThreadProc執行緒中我們迴圈的條件是檔案當前位置小於檔案的長度,所以當迴圈跳出時就說明讀取檔案的執行緒已經執行完畢,這時我們就可以關閉g_pFile指標了。如果將指標的關閉放在ReadDxfThreadProc執行緒中執行,則可能出現檔案指標已關閉,而 SetProgressPosThreadProc執行緒中仍在呼叫檔案指標的情況。  


三、程式實現

        (1)利用AppWizard建立一個MFC AppWizard(EXE)的單文件工程,取名為:ReadFile。

        (2)新建一個對話方塊,併為該對話方塊建立一個類CProgressDlg。

        (3)進度條控制元件和百分數的靜態文字框定義變數:  

CProgressCtrl m_ctrlProgress;

CStrina m_szPercent;      

  (4)為對話方塊新增函式BOOL Create(),以建立非模態對話方塊。函式實現如下:  



BOOL CProgressDlg::Create()
{
    return CDialog::Create(CProgressDlg::IDD, NULL);
}        (5)在對話方塊初始化時要設定進度條的範圍:  


BOOL CProgressDlg::OnInitDialog()
{
    CDialog::OnInitDialog();
    m_ctrlProgress.SetRange(l0, 100);
    return TRUE;
}        (6)由於執行緒是通過向對話方塊傳送訊息來控制進度條的位置和銷燬對話方塊,所以我們必須在ProgressDlg.h檔案中定義兩個自定義訊息:  


 #ifndef WM_UPDATE_DXF_DLG_POS                                       //更新進度條位置的訊息
#define WM_UPDATE_DXF_DLG_POS WM_USER+10000; 
#endif
#ifndef WM_UPDATE_DXF_DLG_DESTROY                                    //銷燬對話方塊的訊息
#define WM_UPDATE_DXF_DLG_DESTROY WMUSER+10001
#endif        (7)建立訊息響應函式。  


在ProgressDlg.cpp檔案中END_MESSAGE_MAP()之前加入以下程式碼:
ON_MESSAGE(WM_UPDATE_DXF_DLG_POS, OnUpdateProgressPos)              //更新進度條位置的訊息響應函式
ON_MESSAGE(WM_UPDATE_DXF_DLG_DESTROY, OnDestroyDlg)                  //銷燬對話方塊的訊息響應函式然後在ProgressDlg.h檔案中DECLARE_MESSAGE_MAP()之前加入以下程式碼:  


afx_msg LRESULT OnUpdateProgressPos(WPARAM wp, LPARAM lp);
afx_msg LRESULT OnDestroyDlg(WPARAM wp, LPARAM lp);在ProgressDlg.cpp檔案中新增訊息響應函式實體:  


LRESULT CProgressDlg::OnUpdateProgressPos(WPARAM wp, LPARAM lp)     //傳入的引數wp即是計算過後當前進度爭的位置
{
    m_ctrIProgress.SetPos((int)wp);                                 //設定進度條位置
    m_szPercent.Forrnat("%d", (int)wp);
    m_szPercent += "%";
    UpdateData( FALSE);                                              //設定百分數顯示的靜態更本
    return 0;
}
LRESULT CProgressDlg::OnDestroyDlg(WPARAM wp, LPARAMp lp)
{
    DestroyWindow();                                                  //銷燬對話方塊
    return 0;
}        (8)建立“確定”按鈕響應函式。         由於我們採用的是非模態對話方塊,所以在此不能呼叫CDialog::OnOK()函式,也不能呼叫DestroyWindow()函式銷燬對話方塊,因為此時可能SetProgressPosThreadProc程序還沒有結束,還在向對話方塊傳送訊息,所以在此只是隱藏對話方塊,等到程序結束以後再發送訊息銷燬對話方塊。函式實體如下:  


void CProgressDlg::OnButtonClose()
{
    ShowWindow(SW_HIDE);
}        (9)在ReadFileView.cpp檔案中定義全域性變數和執行緒函式。程式碼如下:  


CStdioFile* g_pFile = NULL;                                //全域性的正件指標
HANDLE g_hMutex = NULL;                                    //互斥量
DWORD WINAPI SelProqressPosTnreadProc(LPVOID lParam)       //傳送進度各位置訊息的程序
{
    CDialog* pDlg =(CDialog*)lParam;                      //傳入的進度條對話方塊指標
    if(!pDlg) return 0;                                    //異常判斷
    DWORD dPos = 0;                                        //當前檔案指標的位置
    DWORD dLengtn = 1;                                     //檔案的長度
    DWORD dProssPos = 0;                                   //計算出來的進度條的位置
    DWORD dPrePos = 0;                                     //記錄前一個進度條的位置
    DWORD dw;
    dw = WaitForSingleObject(g_hMutex, INFINITE);          //等待互斥量有訊號
    if(dw == WAIT_OBJECT_O);                               //判斷互斥量是否為有訊號
    {
        dLength = g_pFile->GetLength();                    //獲取檔案長度
    }
    else                                                   //廢棄的訊號,說明讀取檔案的執行緒有異常
    {
        ::SendMessage(pDlg->GetSafeHwnd(), WM_UPDATE_DXF_DLG_POS, 100, NULL);        //
        ::SendMessage(pDlg->GetSafeHwnd(), WM_UPDATE_DXF_DLG_DESTROY, NULL, NULL);   //向對話方塊傳送訊息,以銷燬對話方塊
        return O;
    }
    ReleaseMutex(g_nMutex);                                //釋放互斥量
    while(dPos < dLength)                                  //當檔案未讀完時執行迴圈
    {
        if(!pDlg) return 0;                                //異常判斷
        dw = WaitForSingleObject(g_nMutex, INFINITE);      //等待互斥量有訊號
        if{dw == WAIT_OBJECT_O)                           //等待到訊號
        {
            dPos = g_pFile->GetPosition();                 //獲取當前檔案指標的位置
            dProssPos = DWORD(double(dPos)/dLength * 100); //計算進度條的位置
        }
        else                                               //廢棄的訊號
        {
            ::SendMessage(pDlg->GetSafeHwnd(), WM_UPDATE_DXF_DLG_POS, 100, NULL);       
            ::SendMessaqe(pDlg->GeiSafeHwnd(), WM_UPDATE_DXF_DLG_DESTROY, NULL, NULL);  //向對話方塊傳送訊息,以銷燬對話方塊
            return 0;
        }
        ReleaseMutex(g_hMutex);                            //釋放互斥量
        if(dProssPos != dPrePos)                           //進度條位置相同時不傳送更新訊息
        {
            ::SendMessage(pDlg->GetSafeHwnd(), WM_UPDATE_DXF_DLG_POS, dProssPos, NULL);
        }
        dPrePos = dProssPos;                               //當前位置變為前一位置
    }
    //由於計算位置時用的將double強制轉換為DWORD型,有可能沒有計算到100
    //所以需要傳送一個訊息將進度條位置重新整理到100
    ::SendMessage(pDlg->GetSafeHwnd(), WM_UPDATE_DXF_DLG_POS, 100, NULL);
    Sleep(500);
    ::SendMessage(pDlg->GetSafeHwnd(), WM_UPDATE_DXF_DLG_DESTROY, NULL, NULL);          //向對話方塊傳送訊息,以銷燬對話方塊
    //檔案讀取完畢以後要對檔案指標進行關閉和銷燬
    dw = WaitForSingleObject(g_hMutex, INFINITE);
    if(dw == WAIT_OBJECT_0)
    {
        g_pFile->Close();
        delete g_pFile;
        g_pFile = NULL;
    }
    else                                                    //廢棄的訊號
    {
        g_pFile->Close();
        delete g_pFile;
        g_pFile = NULL;
    }
    ReleaseMutex(g_hMutex);
    return 0;
}
    
DWORD WINAPI ReadDxfThreadProc(LPVOID lParam)              //讀取檔案的執行緒 
{
    CString strTernp = "";                                 //存放讀取的字元
    BOOL bisEnd = TRUE;                                    //指示檔案是否讀取完畢
    DWORD dw,//互斥量信爭結襄
    while(1)
    {
        if(!g_pFile)
            return 0;                                      //異常判斷
        dw = WaitForSingleObject(g_hMutex,INFINITE);       
        if(dw == WAIT_OBJECT_0)                            //互斥量等待到訊號
        {
            blsEnd = g_pFile->ReadString(strTemp);         //讀取檔案
        }
        else if(dw == WAIT_ABANDONED)
            return 0;
        ReleaseMUtex( g_hMutex):
        if(blsEnd == FALSE)
            break;
    }
    return 0;

}  

(10)在CReadFileView中新增進度條對話方塊物件成員。這個物件不能在呼叫時宣告,因為主執行緒執行完後就會銷燬物件,這樣線上程中呼叫對話方塊指標就會出現異常。 

 在ReadFileView.h檔案中新增:CProgressDlg m_ProgressDlg;  

 (11)在主框架選單中新增下拉選單“測試”,在“測試”選單下新增子選單“讀取dxf檔案”,然後為子選單新增響應函式OnReadDxfFile(),函式程式碼如下: