MFC——9.多線程與線程同步

Lesson9:多線程與線程同步

 

程序、進程和線程是操作系統的重點,在計算機編程中,多線程技術是提高程序性能的重要手段。本文主要講解操作系統中程序、進程和線程之間的關係,並通過互斥對象和事件對象實例說明多線程和線程同步技術。

 

1.      程序、進程和線程

1.1  程序和進程

程序是計算機指令的集合,它以文件的形式存儲在磁盤上。進程通常被定義爲一個正在運行的程序的實例,是一個程序在其自身的地址空間中的一次執行活動。進程是資源申請、調度和獨立運行的單位,因此,它使用系統中的運行資源;而程序不能申請系統資源,不能被系統調度,也不能作爲獨立運行的單位,因此,它不佔用系統的運行資源。

進程由兩個部分組成:

1、操作系統用來管理進程的內核對象。內核對象也是系統用來存放關於進程的統計信息的地方。

2、地址空間。它包含所有可執行模塊或DLL模塊的代碼和數據。它還包含動態內存分配的空間。如線程堆棧和堆分配空間。

每個進程有它自己的私有地址空間。進程A可能有一個存放在它的地址空間中的數據結構,地址是0x12345678,而進程B則有一個完全不同的數據結構存放在它的地址空間中,地址是0x12345678。當進程A中運行的線程訪問地址爲0x12345678的內存時,這些線程訪問的是進程A的數據結構。當進程B中運行的線程訪問地址爲0x12345678的內存時,這些線程訪問的是進程B的數據結構。進程A中運行的線程不能訪問進程B的地址空間中的數據結構,反之亦然。一個進程不能讀取、寫入、或者以任何方式訪問駐留在該分區中的另一個進程的數據。對於所有應用程序來說,該分區是維護進程的大部分數據的地方。

 

1.2  進程和線程

進程是不活潑的。進程從來不執行任何東西,它只是線程的容器。若要使進程完成某項操作,它必須擁有一個在它的環境中運行的線程,此線程負責執行包含在進程的地址空間中的代碼。單個進程可能包含若干個線程,這些線程都“同時” 執行進程地址空間中的代碼。每個進程至少擁有一個線程,來執行進程的地址空間中的代碼。當創建一個進程時,操作系統會自動創建這個進程的第一個線程,稱爲主線程。此後,該線程可以創建其他的線程。操作系統爲每一個運行線程安排一定的CPU時間——時間片。系統通過一種循環的方式爲線程提供時間片,線程在自己的時間內運行,因時間片相當短,因此,給用戶的感覺,就好像線程是同時運行的一樣。如果計算機擁有多個CPU,線程就能真正意義上同時運行了。

線程由兩個部分組成:

1、線程的內核對象,操作系統用它來對線程實施管理。內核對象也是系統用來存放線程統計信息的地方。當創建線程時,系統創建一個線程內核對象。該線程內核對象不是線程本身,而是操作系統用來管理線程的較小的數據結構。可以將線程內核對象視爲由關於線程的統計信息組成的一個小型數據結構。

2、  線程堆棧,它用於維護線程在執行代碼時需要的所有參數和局部變量。

 

線程總是在某個進程環境中創建。系統從進程的地址空間中分配內存,供線程的堆棧使用。新線程運行的進程環境與創建線程的環境相同。因此,新線程可以訪問進程的內核對象的所有句柄、進程中的所有內存和在這個相同的進程中的所有其他線程的堆棧。這使得單個進程中的多個線程確實能夠非常容易地互相通信。線程只有一個內核對象和一個堆棧,保留的記錄很少,因此所需要的內存也很少。因爲線程需要的開銷比進程少,因此在編程中經常採用多線程來解決編程問題,而儘量避免創建新的進程。

 

2.      多線程

主線程可以創建多個子線程,每個線程有自己的時間片,當時間片到了就執行下一個線程。爲了讓線程能嚴格交替執行,可以用互斥對象和事件對象實現。

2.1   互斥對象實現線程同步

//實現主線程裏的新線程在交替時間片內運行
#include <windows.h>
#include <iostream>
using namespace std;
 
//線程函數 原型聲明
DWORD WINAPI Fun1Proc(LPVOIDlpParameter);   // thread data
DWORD WINAPI Fun2Proc(LPVOIDlpParameter);   // thread data
 
//int index = 0;       //定義一個循環計數
int tickets = 100;
HANDLE hMutex;         //定義一個全局的互斥對象
 
void main()
{
   //主程序運行時,自動創建主線程,我們用CreateThread()函數創建線程
                  
         HANDLEhThread1;
         HANDLEhThread2;
         hThread1= CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);    //(1null表示使用缺省的安全性,2 0表示和調用線程一樣的大小,3指定線程的入口函數地址,4傳遞給線程的參數, 5 0表示一旦創建立即運行如果設置爲CREATE_SUSPENDED 表示遇到 ResumeThread function 時調用,6線程的ID ,不使用用NULL )
         hThread2= CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
        
         CloseHandle(hThread1);     //這裏剛創建線程就關閉,其實並沒有終止創建的線程。只是主線程中對新線程的引用不感興趣,關閉後可以減小線程內核的引用計數
         CloseHandle(hThread2);   
 
         hMutex=CreateMutex(NULL,FALSE, NULL);         //創建互斥對象,如果false改爲true,則表示主線程擁有互斥對象,所以如果主線程不釋放互斥對象,別的線程是得不到互斥對象的。
        
         //通過命名的互斥對象,讓應用程序只有一個實例運行
         /*hMutex= CreateMutex(NULL, FALSE, "tickets");
         if(hMutex)
         {
                   if(ERROR_ALREADY_EXISTS == GetLastError())
                   {
                            cout<< "only instance is running"<< endl;
                            return;
                   }
         }*/
 
         //如果爲true,則表示擁有互斥對象,若再次請求互斥對象,互斥對象的計數器會加一,表示又請求了一次,而且請求成功
         /*hMutex= CreateMutex(NULL,TRUE, NULL);
         WaitForSingleObject(hMutex,INFINITE);        //雖然主線程擁有互斥對象,所以它現在爲未通知狀態,但請求互斥對象的ID 和擁有互斥對象的ID 是同一個ID ,所以可以請求到
         ReleaseMutex(hMutex);      //擁有了兩次,釋放兩次(多次請求,多次釋放。通過計數器記錄的)
         ReleaseMutex(hMutex);*/
 
         //主線程不能在賣完100張票前結束,主線程睡眠足夠時間讓子線程有足夠時間片執行
         Sleep(4000);
         //Sleep(10);             //暫停函數,這裏暫停10ms,   sleep time in milliseconds
         system("pause");
}
 
//線程函數 實現
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
         while(TRUE)
         {
                   //線程1得到互斥對象,互斥對象的ID就爲線程1的線程ID,互斥對象變爲未通知狀態,
                   WaitForSingleObject(hMutex,INFINITE);      //互斥對象相當於一個鑰匙,有了它,才能往下執行,此時鑰匙在我這,即使線程睡覺了,別人進不了這個房間,當離開房間,交出鑰匙,別人才能拿到鑰匙,進去房間
                  
                   if(tickets > 0)
                   {
                            Sleep(1);   //執行sleep表示操作系統暫時放棄當前線程一段時間,執行下一個線程,此時這個線程停止在這裏,當下次這個線程運行時,接着運行下一行
                            cout<< "Thread1 sell ticket:" << tickets-- << endl;
                   }
                   else
                            break;
                   ReleaseMutex(hMutex);    //釋放互斥對象,釋放後操作系統將互斥對象線程ID爲0,互斥對象爲已通知狀態,這樣線程2才能獲得互斥對象
         }
 
         /*WaitForSingleObject(hMutex,INFINITE);//如果操作系統認爲線程結束,那麼在這個線程裏請求的互斥對象引用計數和線程ID爲零,線程2就可以得到互斥對象了
         cout<< "Thread1 is running!" <<endl;*/
        
         return0;
}
 
DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
         while(TRUE)
         {
                   WaitForSingleObject(hMutex,INFINITE);    //線程1在sleep時,執行線程2,但線程2這裏發生互斥,執行不了,然後當線程1睡醒了就繼續執行線程1,
                   if(tickets > 0)
                   {
                            Sleep(1);
                            cout<< "Thread2 sell ticket:" << tickets-- << endl;
                   }
                   else
                            break;
                   ReleaseMutex(hMutex);
         }
 
         /*WaitForSingleObject(hMutex,INFINITE);
         cout<< "Thread2 is running!" << endl;*/
         return0;
}

程序中通過互斥對象實現在每個子線程的時間片內交替循環執行一次,tickes是全局變量。互斥對象(mutex)屬於內核對象,它能夠確保線程擁有對單個資源的互斥訪問權。互斥對象包含一個使用數量,一個線程ID和一個計數器。ID用於標識系統中的哪個線程當前擁有互斥對象,計數器用於指明該線程擁有互斥對象的次數。

 

下面是程序中幾個關鍵函數

1.創建互斥對象

HANDLE CreateThread(         //The CreateThread function creates a thread to execute within theaddress space of the calling process.
          LPSECURITY_ATTRIBUTESlpThreadAttributes,  // pointer to securityattributes,結構體指針,
          DWORDdwStackSize,     // initial thread stacksize,指定初始棧大小
          LPTHREAD_START_ROUTINElpStartAddress,    // pointer to threadfunction,指向一個應用程序的線程的指針
          LPVOIDlpParameter, // argument for new thread,指定一個單獨的參數值傳遞給線程
          DWORDdwCreationFlags, // creation flags,指定控制線程創建的附加標記
          LPDWORDlpThreadId    // pointer to receivethread ID,函數的返回值,指向一個變量,接收線程的標示符ID
          );

2.創建互斥對象

HANDLE CreateMutex(     //創建互斥對象,完成線程的同步 TheCreateMutex function creates a named or unnamed mutex object.
        LPSECURITY_ATTRIBUTES lpMutexAttributes,    // pointer to security attributes,結構指針,NULL 表示默認的安全性
        BOOL bInitialOwner,        //flag for initial ownership,互斥對象初始擁有者,真表示調用者創建互斥對象,調用的線程獲得互斥對象的所有權,否則不獲得
        LPCTSTR lpName     //pointer to mutex-object name),互斥對象名字,null表示沒有名字
)

3.請求互斥對象   

WaitForSingleObject        //下面兩種情況發生時,這個函數返回
The specified object is in the signaled state.     //指定的對象處於有信號狀態
The time - out interval elapses.               //超時的時間間隔流逝了

DWORD WaitForSingleObject(     //The WaitForSingleObject function returns when one of the following occurs :
       HANDLE hHandle,              // handle to object to wait for,等待的互斥對象的句柄
       DWORD dwMilliseconds         // time-out interval in milliseconds,超時的時間間隔,如果時間流逝了,即使所等待的對象處於非信號狀態的,函數返回。參數爲0,表示測試對象狀態,立即返回,參數爲INFINITE,表示一直等待,直到等待對象處於有信號狀態
);


4.釋放互斥對象           //那個線程擁有互斥對象,哪個線程釋放互斥對象

ReleaseMutex             //釋放指定互斥對象的所有權,執行成功返回非0.失敗返回0
BOOL ReleaseMutex(         //The ReleaseMutex function releasesownership of the specified mutex object.
       HANDLE hMutex             // handle to mutex object
);
 

2.2   事件對象實現線程同步

#include<Windows.h>
#include<iostream>
using namespace std;
 
//線程函數 原型聲明
DWORD WINAPI Fun1Proc(LPVOIDlpParameter);   // thread data
DWORD WINAPI Fun2Proc(LPVOIDlpParameter);   // thread data
 
int tickets = 100;
HANDLE g_hEvent;                            //定義一個全局的互斥對象的句柄,保存時間對象的句柄
void main()
{
         HANDLEhThread1;
         HANDLEhThread2;
         hThread1= CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);    //(1null表示使用缺省的安全性,2 0表示和調用線程一樣的大小,3指定線程的入口函數地址,4傳遞給線程的參數, 5 0表示一旦創建立即運行如果設置爲CREATE_SUSPENDED 表示遇到 ResumeThread function 時調用,6線程的ID ,不使用用NULL )
         hThread2= CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
 
         CloseHandle(hThread1);    //這裏剛創建線程就關閉,其實並沒有終止創建的線程。只是主線程中對新線程的引用不感興趣,關閉後可以減小線程內核的引用計數
         CloseHandle(hThread2);
 
 
/*  HANDLE CreateEvent(The CreateEvent function creates a named or unnamedevent object.
                   LPSECURITY_ATTRIBUTESlpEventAttributes,     // pointer tosecurity attributes   NULL表示默認安全性                    
                   BOOLbManualReset,      // flag for manual-reset event,指定是否人工重置或自動重置的對象被創建。真表示人工重置,假表示自動重置
                   BOOLbInitialState,                          // flag for initial state,指定事件初始化狀態
                   LPCTSTRlpName                               // pointer to event-object name,事件對象的名字
                   );*/
 
        
         //g_hEvent= CreateEvent(NULL,FALSE,FALSE,NULL);             //第三個參數指定初始化爲無信號狀態
         g_hEvent= CreateEvent(NULL, FALSE, FALSE, "tickets");       //創建命名的互斥對象
         if(g_hEvent)
         {
                   if(ERROR_ALIAS_EXISTS==GetLastError())
                   {
                            cout<< "only instance can run!" << endl;
                            return;
                   }
         }
        
         SetEvent(g_hEvent);     //設定爲有信號狀態,人工重置的對象爲有信號狀態時,所以等待該時間的線程,都可以調度,可以同時運行。
                              //自動重置的對象,爲有信號狀態時,當線程得到該對象,操作系統自動設置爲無信號狀態,這樣別的線程無法得到此對象。
         Sleep(4000);
         CloseHandle(g_hEvent);
}
 
//線程函數 實現
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
         while(TRUE)
         {
                   //線程1得到互斥對象,互斥對象的ID就爲線程1的線程ID,互斥對象變爲未通知狀態,
                   WaitForSingleObject(g_hEvent,INFINITE);      //互斥對象相當於一個鑰匙,有了它,才能往下執行,此時鑰匙在我這,即使線程睡覺了,別人進不了這個房間,當離開房間,交出鑰匙,別人才能拿到鑰匙,進去房間
 
                   if(tickets > 0)
                   {
                            Sleep(1);     //執行sleep表示操作系統暫時放棄當前線程一段時間,執行下一個線程,此時這個線程停止在這裏,當下次這個線程運行時,接着運行下一行
                            cout<< "Thread1 sell ticket:" << tickets-- << endl;
                   }
                   else
                            break;
                   SetEvent(g_hEvent);        //將事件對象設置爲有信號狀態,釋放互斥對象的控制權,不再運行此線程,這個線程釋放了互斥對象的控制權後,如果其他進程在等待互斥對象置位,則等待的線程可以得到該互斥對象,等待函數返回,互斥對象被新的線程所擁有。
         }
         return0;
}
 
DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
         while(TRUE)
         {
                   WaitForSingleObject(g_hEvent,INFINITE);    //線程1在sleep時,執行線程2,但線程2這裏發生互斥,執行不了,然後當線程1睡醒了就繼續執行線程1
 
                   if(tickets > 0)
                   {
                            Sleep(1);
                            cout<< "Thread2 sell ticket:" << tickets-- << endl;
                   }
                   else
                            break;
                   SetEvent(g_hEvent);
         }
         return0;
}

事件對象也屬於內核對象,包含一個使用計數,一個用於指明該事件是一個自動重置的事件還是一個人工重置的事件的布爾值,另一個用於指明該事件處於已通知狀態還是未通知狀態的布爾值。有兩種不同類型的事件對象。一種是人工重置的事件,另一種是自動重置的事件。當人工重置的事件得到通知時,等待該事件的所有線程均變爲可調度線程。當一個自動重置的事件得到通知時,等待該事件的線程中只有一個線程變爲可調度線程。

互斥對象和事件對象屬於內核對象,利用內核對象進行線程同步,速度較慢,但利用互斥對象和事件對象這樣的內核對象,可以在多個進程中的各個線程間進行同步。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章