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、 線程堆棧,它用於維護線程在運行代碼時須要的全部參數和局部變量。
線程總是在某個進程環境中創建。
系統從進程的地址空間中分配內存,供線程的堆棧使用。新線程執行的進程環境與創建線程的環境同樣。因此。新線程可以訪問進程的內核對象的全部句柄、進程中的全部內存和在這個同樣的進程中的全部其它線程的堆棧。這使得單個進程中的多個線程確實可以非常easy地互相通信。
線程僅僅有一個內核對象和一個堆棧。保留的記錄非常少,因此所須要的內存也非常少。由於線程須要的開銷比進程少。因此在編程中常常採用多線程來解決編程問題,而盡量避免創建新的進程。
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; }
事件對象也屬於內核對象。包括一個使用計數。一個用於指明該事件是一個自己主動重置的事件還是一個人工重置的事件的布爾值。還有一個用於指明該事件處於已通知狀態還是未通知狀態的布爾值。
有兩種不同類型的事件對象。一種是人工重置的事件。還有一種是自己主動重置的事件。
當人工重置的事件得到通知時,等待該事件的全部線程均變為可調度線程。
當一個自己主動重置的事件得到通知時。等待該事件的線程中僅僅有一個線程變為可調度線程。
相互排斥對象和事件對象屬於內核對象。利用內核對象進行線程同步。速度較慢,但利用相互排斥對象和事件對象這種內核對象,能夠在多個進程中的各個線程間進行同步。