多線程在編程中有相當重要的地位,我們在實際開發時或者找工作面試時總能遇到多線程的問題,對多線程的理解程度從一個側面反映了程序員的編程水平。
其實C++語言本身並沒有提供多線程機制,但Windows系統為我們提供了相關API,我們可以使用它們來進行多線程編程。本文就以實例的形式講解多線程編程的知識。
創建線程的API函數
- HANDLE CreateThread(
- __in SEC_ATTRS
- SecurityAttributes,
- __in ULONG
- StackSize, // initial stack size
- __in SEC_THREAD_START
- StartFunction, // thread function
- __in PVOID
- ThreadParameter, // thread argument
- __in ULONG
- CreationFlags, // creation option
- __out PULONG
- ThreadId // thread identifier
- );
在這里我們只用到了第三個和第四個參數,第三個參數傳遞了一個函數的地址,也是我們要指定的新的線程,第四個參數是傳給新線程的參數指針。
多線程編程實例1:
- #include <iostream>
- #include <windows.h>
- using namespace std;
- DWORD WINAPI Fun(LPVOID lpParamter)
- {
- while(1) { cout<<"Fun display!"<<endl; }
- }
- int main()
- {
- HANDLE hThread = CreateThread(NULL, 0, Fun, NULL, 0, NULL);
- CloseHandle(hThread);
- while(1) { cout<<"main display!"<<endl; }
- return 0;
- }
我們可以看到主線程(main函數)和我們自己的線程(Fun函數)是隨機地交替執行的,但是兩個線程輸出太快,使我們很難看清楚,我們可以使用函數Sleep來暫停線程的執行。
- VOID WINAPI Sleep(
- __in DWORD dwMilliseconds
- );
dwMilliseconds表示千分之一秒,所以 Sleep(1000); 表示暫停1秒。
多線程編程實例2:
- #include <iostream>
- #include <windows.h>
- using namespace std;
- DWORD WINAPI Fun(LPVOID lpParamter)
- {
- while(1) { cout<<"Fun display!"<<endl; Sleep(1000);}
- }
- int main()
- {
- HANDLE hThread = CreateThread(NULL, 0, Fun, NULL, 0, NULL);
- CloseHandle(hThread);
- while(1) { cout<<"main display!"<<endl; Sleep(2000);}
- return 0;
- }
執行上述代碼,這次我們可以清楚地看到在屏幕上交錯地輸出Fun display!和main display!,我們發現這兩個函數確實是並發運行的,細心的讀者可能會發現我們的程序是每當Fun函數和main函數輸出內容后就會輸出換行,但是我們看到的確是有的時候程序輸出換行了,有的時候確沒有輸出換行,甚至有的時候是輸出兩個換行。這是怎么回事?下面我們把程序改一下看看。
多線程編程實例3:
- #include <iostream>
- #include <windows.h>
- using namespace std;
- DWORD WINAPI Fun(LPVOID lpParamter)
- {
- while(1) { cout<<"Fun display!\n"; Sleep(1000);}
- }
- int main()
- {
- HANDLE hThread = CreateThread(NULL, 0, Fun, NULL, 0, NULL);
- CloseHandle(hThread);
- while(1) { cout<<"main display!\n"; Sleep(2000);}
- return 0;
- }
我們再次運行這個程序,我們發現這時候正如我們預期的,正確地輸出了我們想要輸出的內容並且格式也是正確的。下面我就來講一下此前我們的程序為什么沒有正確的運行。多線程的程序時並發地運行的,多個線程之間如果公用了一些資源的話,我們並不能保證這些資源都能正確地被利用,因為這個時候資源並不是獨占的,下面舉個例子吧。
多線程編程實例4:
假如有一個資源 int a = 3
有一個線程函數 selfAdd() 該函數是使 a += a;
又有一個線程函數 selfSub() 該函數是使 a -= a;
我們假設上面兩個線程正在並發欲行,如果selfAdd在執行的時候,我們的目的是想讓a編程6,但此時selfSub得到了運行的機會,所以a變成了0,等到selfAdd的到執行的機會后,a += a ,但是此時a確是0,並沒有如我們所預期的那樣的到6。
我們回到前面實例2,在這里,我們可以把屏幕看成是一個資源,這個資源被兩個線程所共用,加入當Fun函數輸出了Fun display!后,將要輸出endl(也就是清空緩沖區並換行,在這里我們可以不用理解什么事緩沖區),但此時main函數確得到了運行的機會,此時Fun函數還沒有來得及輸出換行就把CPU讓給了main函數,而這時main函數就直接在Fun display!后輸出main display!,至於為什么有的時候程序會連續輸出兩個換行,讀者可以采用同樣的分析方法來分析,在這里我就不多講了,留給讀者自己思考了。
那么為什么我們把實例2改成實例3就可以正確的運行呢?原因在於,多個線程雖然是並發運行的,但是有一些操作是必須一氣呵成的,不允許打斷的,所以我們看到實例2和實例3的運行結果是不一樣的。
那么,是不是實例2的代碼我們就不可以讓它正確的運行呢?答案當然是否,下面我就來講一下怎樣才能讓實例2的代碼可以正確運行。這涉及到多線程的同步問題。對於一個資源被多個線程共用會導致程序的混亂,我們的解決方法是只允許一個線程擁有對共享資源的獨占,這樣就能夠解決上面的問題了。
- HANDLE CreateMutex(
- LPSECURITY_ATTRIBUTES lpMutexAttributes,
- BOOL bInitialOwner, // initial owner
- LPCTSTR lpName // object name
- );
該函數用於創造一個獨占資源,第一個參數我們沒有使用,可以設為NULL,第二個參數指定該資源初始是否歸屬創建它的進程,第三個參數指定資源的名稱。
- HANDLE hMutex = CreateMutex(NULL,TRUE,"screen");
這條語句創造了一個名為screen並且歸屬於創建它的進程的資源。
- BOOL ReleaseMutex(
- HANDLE hMutex // handle to mutex
- );
該函數用於釋放一個獨占資源,進程一旦釋放該資源,該資源就不再屬於它了,如果還要用到,需要重新申請得到該資源。申請資源的函數如下:
- DWORD WaitForSingleObject(
- HANDLE hHandle, // handle to object
- DWORD dwMilliseconds // time-out interval
- );
第一個參數指定所申請的資源的句柄,第二個參數一般指定為INFINITE,表示如果沒有申請到資源就一直等待該資源,如果指定為0,表示一旦得不到資源就返回,也可以具體地指定等待多久才返回,單位是千分之一秒。好了,該到我們來解決實例2的問題的時候了,我們可以把實例2做一些修改,如下面的實例。
多線程編程實例5:
- #include <iostream>
- #include <windows.h>
- using namespace std;
- HANDLE hMutex;
- DWORD WINAPI Fun(LPVOID lpParamter)
- {
- while(1) {
- WaitForSingleObject(hMutex, INFINITE);
- cout<<"Fun display!"<<endl;
- Sleep(1000);
- ReleaseMutex(hMutex);
- }
- }
- int main()
- {
- HANDLE hThread = CreateThread(NULL, 0, Fun, NULL, 0, NULL);
- hMutex = CreateMutex(NULL, FALSE, "screen");
- CloseHandle(hThread);
- while(1) {
- WaitForSingleObject(hMutex, INFINITE);
- cout<<"main display!"<<endl;
- Sleep(2000);
- ReleaseMutex(hMutex);
- }
- return 0;
- }
運行此代碼會得到我們預期的輸出內容。