VC++深入詳解(14):多線程與線程同步(重新修改版)

來源:互聯網
上載者:User

考慮到內容的連貫性,我對幾乎重寫了這篇部落格,在這一小節,主要介紹線程以及線程間的同步,而把那個聊天工具放到下一節。

什麼是程式?程式是計算計指令的集合,它以檔案的形式儲存在磁碟上。
什麼是進程?進程是一個正在啟動並執行程式的執行個體,是一個程式在其自身的地址空間內中的一次執行活動。因此,一個程式可以對應多個進程,比如我們可以把自己編寫的簡單的“hello, world”程式執行很多遍。
進程是資源申請、調度很啟動並執行基本單位,因此:

#include <stdio.h>int a = 0;int main(){printf("%d",a);++a;return 0;}

儘管a是“全域”變數,但假如我們把這個程式執行2遍,每次列印出來的結果都是0。
在Windows系統下,進程有兩部分組成:
(1)作業系統用來管理進程的核心對象。
這些對象用來存放進程統計資訊。他們是作業系統內部分配的記憶體塊,只能被核心訪問使用,用用程式無法找到該資料結構,並直接改變其內容。Windows提供了一些函數來對核心對象進行操作。
(2)地址空間
它包含所有可執行檔模組或DLL模組的代碼和資料,也包含動態分配的空間。例如線程的棧和堆。
關於進程的知識我們會在後面的章節仔細講解。

實際上,進程從來不執行任何東西,若要使進程完成某項操作,它必須擁有一個在它的環境中啟動並執行線程,此線程負責執行包含在進程的地址空間中的代碼。也就是說,真正完成代碼執行的是線程,而進程只是線程的容器。線程是使用系統資源的基本單位。

線程也由兩部分組成:
(1)線程的核心對象。作業系統用它來對線程進行管理,存放線程的統計資訊。
(2)線程棧。它用於維護線程執行代碼時所需要的所有函數和參數的局部變數。

線程只有一個核心對象和一個棧,開銷相對較少,因此在編程中經常採用多線程來解決編程問題,而盡量避免建立新的進程。
與線程相關的基本函數包括:
CreateThread:建立線程
CloseHandle:關閉線程控制代碼。注意,這隻會使指定的線程控制代碼無效(減少該控制代碼的引用計數),啟動控制代碼的檢查操作,如果一個對象所關聯的最後一個控制代碼被關閉了,那麼這個對象會從系統中被刪除。關閉控制代碼不會終止相關的線程。

線程是如何啟動並執行呢?這又與你的CPU有關係了,如果你是一個單核CPU,那麼系統會採用時間片輪詢的方式運行每個線程;如果你是多核CPU,那麼線程之間就有可能並發運行了。這樣就會出現很多問題,比如兩個線程同時訪問一個全域變數之類的。它們需要線程的同步來解決。所謂同步,並不是多個線程一起同時執行,而是他們協同步調,按預定的先後次序執行。
Windows下線程同步的基本方法有3種:互斥對象、事件對象、關鍵程式碼片段(臨界區),下面一一介紹:

互斥對象屬於核心對象,包含3個成員:
1.使用數量:記錄了有多少個線程在調用該對象
2.一個線程ID:記錄互斥對象維護的線程的ID
3.一個計數器:前線程調用該對象的次數
與之相關的函數包括:
建立互斥對象:CreateMutex
判斷能否獲得互斥對象:WaitForSingleObject
對於WaitForSingleObject,如果互斥對象為有訊號狀態,則擷取成功,函數將互斥對象設定為無訊號狀態,程式將繼續往下執行;如果互斥對象為無訊號狀態,則擷取失敗,線程會停留在這裡等待。等待的時間可以由參數控制。
釋放互斥對象:ReleaseMutex
當要保護的代碼執行完畢後,通過它來釋放互斥對象,使得互斥對象變為有訊號狀態,以便於其他線程可以擷取這個互斥對象。注意,只有當某個線程擁有互斥對象時,才能夠釋放互斥對象,在其他線程調用這個函數不得達到釋放的效果,這可以通過互斥對象的線程ID來判斷。

我們看一個例子:

#include <Windows.h>#include <stdio.h>//線程函式宣告DWORD WINAPI Thread1Proc(  LPVOID lpParameter);DWORD WINAPI Thread2Proc(  LPVOID lpParameter);//全域變數int tickets = 100;HANDLE hMutex;int main(){HANDLE hThread1;HANDLE hThread2;//建立互斥對象hMutex = CreateMutex( NULL,//預設安全層級  FALSE,//建立它的線程不擁有互斥對象  NULL);//沒有名字//建立線程1hThread1 = CreateThread(NULL,//預設安全層級0,//預設棧大小Thread1Proc,//線程函數 NULL,//函數沒有參數0,//建立後直接運行NULL);//線程標識,不需要//建立線程2hThread2 = CreateThread(NULL,//預設安全層級0,//預設棧大小Thread2Proc,//線程函數 NULL,//函數沒有參數0,//建立後直接運行NULL);//線程標識,不需要//主線程休眠4秒Sleep(4000);//主線程休眠4秒Sleep(4000);//關閉線程控制代碼CloseHandle(hThread1);CloseHandle(hThread2);//釋放互斥對象ReleaseMutex(hMutex);return 0;}//線程1入口函數DWORD WINAPI Thread1Proc(  LPVOID lpParameter){while(TRUE){WaitForSingleObject(hMutex,INFINITE);if(tickets > 0){Sleep(10);printf("thread1 sell ticket : %d\n",tickets--);ReleaseMutex(hMutex);}else{ReleaseMutex(hMutex);break;}}return 0;}//線程2入口函數DWORD WINAPI Thread2Proc(  LPVOID lpParameter){while(TRUE){WaitForSingleObject(hMutex,INFINITE);if(tickets > 0){Sleep(10);printf("thread2 sell ticket : %d\n",tickets--);ReleaseMutex(hMutex);}else{ReleaseMutex(hMutex);break;}}return 0;}

使用互斥對象時需要小心:
調用假如一個線程本身已經擁有該互斥對象,則如果它繼續調用WaitForSingleObject,則會增加互斥對象的引用計數,此時,你必須多次調用ReleaseMutex來釋放互斥對象,以便讓其他線程可以擷取:

//建立互斥對象hMutex = CreateMutex( NULL,//預設安全層級  TRUE,//建立它的線程擁有互斥對象  NULL);//沒有名字WaitForSingleObject(hMutex,INFINITE);//釋放互斥對象ReleaseMutex(hMutex);//釋放互斥對象ReleaseMutex(hMutex);

下面看事件對象,它也屬於核心對象,包含3各成員:
1.使用計數
2.用於指明該事件是自動重設事件還是人工重設事件的布爾值
3.用於指明該事件處於已通知狀態還是未通知狀態。
自動重設和人工重設的事件對象有一個重要的區別:當人工重設的事件對象得到通知時,等待該事件對象的所有線程均變為可調度線程;而當一個自動重設的事件對象得到通知時,等待該事件對象的線程中只有一個線程變為可調度線程。
與事件對象相關的函數包括:
建立事件對象:CreateEvent
HANDLE CreateEvent(  LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL bInitialState,LPCTSTR lpName);
設定事件對象:SetEvent:將一個這件對象設為有訊號狀態
BOOL SetEvent(  HANDLE hEvent  );
重設事件對象狀態:ResetEvent:將指定的事件對象設為無訊號狀態
BOOL ResetEvent(  HANDLE hEvent );

下面仍然使用買火車票的例子:

#include <Windows.h>#include <stdio.h>//線程函式宣告DWORD WINAPI Thread1Proc(  LPVOID lpParameter);DWORD WINAPI Thread2Proc(  LPVOID lpParameter);//全域變數int tickets = 100;HANDLE g_hEvent;int main(){HANDLE hThread1;HANDLE hThread2;//建立事件對象g_hEvent = CreateEvent( NULL,//預設安全層級TRUE,//人工重設FALSE,//初始為無訊號NULL );//沒有名字//建立線程1hThread1 = CreateThread(NULL,//預設安全層級0,//預設棧大小Thread1Proc,//線程函數 NULL,//函數沒有參數0,//建立後直接運行NULL);//線程標識,不需要//建立線程2hThread2 = CreateThread(NULL,//預設安全層級0,//預設棧大小Thread2Proc,//線程函數 NULL,//函數沒有參數0,//建立後直接運行NULL);//線程標識,不需要//主線程休眠4秒Sleep(4000);//關閉線程控制代碼//當不再引用這個控制代碼時,立即將其關閉,減少其引用計數CloseHandle(hThread1);CloseHandle(hThread2);//關閉事件物件控點CloseHandle(g_hEvent);return 0;}//線程1入口函數DWORD WINAPI Thread1Proc(  LPVOID lpParameter){while(TRUE){WaitForSingleObject(g_hEvent,INFINITE);if(tickets > 0){Sleep(1);printf("thread1 sell ticket : %d\n",tickets--);}elsebreak;}return 0;}//線程2入口函數DWORD WINAPI Thread2Proc(  LPVOID lpParameter){while(TRUE){WaitForSingleObject(g_hEvent,INFINITE);if(tickets > 0){Sleep(1);printf("thread2 sell ticket : %d\n",tickets--);}elsebreak;}return 0;}

程式運行後並沒有出現兩個線程買票的情況,而是等待了4秒之後直接退出了,這是什麼原因呢?問題出在了我們建立的事件對象一開始就是無訊號狀態的,因此2個線程線程運行到WaitForSingleObject時就會一直等待,直到自己的時間片結束。所以什麼也不會輸出。
如果想讓線程能夠執行,可以在建立線程時將第3個參數設為TRUE,或者在建立完成後調用

SetEvent(g_hEvent);

程式的確可以實現買票了,但是有些時候,會列印出某個線程賣出第0張票的情況,這是因為當人工重設的事件對象得到通知時,等待該對象的所有線程均可變為可調度線程,兩個線程同時運行,線程的同步失敗了。

也許有人會想到,線上程得到CPU之後,能否使用ResetEvent是得線程將事件對象設為無訊號狀態,然後當所保護的代碼運行完成後,再將事件對象設為有訊號狀態?我們可以試試:

//線程1入口函數DWORD WINAPI Thread1Proc(  LPVOID lpParameter){while(TRUE){WaitForSingleObject(g_hEvent,INFINITE);ResetEvent(g_hEvent);if(tickets > 0){Sleep(10);printf("thread1 sell ticket : %d\n",tickets--);SetEvent(g_hEvent);}else{SetEvent(g_hEvent);break;}}return 0;}

線程2的類似,這裡就省略了。運行程式,發現依然會出現賣出第0張票的情況。這是為什麼呢?我們仔細思考一下:單核CPU下,可能線程1執行完WaitForSingleObject,還沒來得及執行ResetEvent時,就切換到線程2了,此時,由於線程1並沒有執行ResetEvent,所以線程2也可以得到事件對象了。而在多CPU平台下,假如兩個線程同時執行,則有可能都執行到本應被保護的代碼地區。
所以,為了實現線程間的同步,不應該使用人工重設的事件對象,而應該使用自動重設的事件對象:

hThread2 = CreateThread(NULL,0,Thread2Proc,NULL0,NULL);

並將原來寫的ResetEvent和SetEvent全都注釋起來。我們發現程式只列印了一次買票過程。我們分析一下原因:
當一個自動重設的事件得到通知後,等待該該事件的線程中只有一個變為可調度線程。在這裡,線程1變為可調度線程後,作業系統將事件設為了無訊號狀態,當線程1休眠時,所以線程2隻能等待,時間片結束以後,又輪到線程1運行,輸出thread1 sell ticket :100。然後迴圈,又去WaitForSingleObject,而此時事件對象處於無訊號狀態,所以線程不能繼續往下執行,只能一直等待,等到自己時間片結束,直到主線程睡醒了,結束整個程式。
正確的使用方法是:當訪問完對保護的程式碼片段後,立即調用SetEvent將其設為有訊號狀態。允許其他等待該對象的線程變為可調度狀態:

DWORD WINAPI Thread1Proc(  LPVOID lpParameter){while(TRUE){WaitForSingleObject(g_hEvent,INFINITE);if(tickets > 0){Sleep(10);printf("thread1 sell ticket : %d\n",tickets--);SetEvent(g_hEvent);}else{SetEvent(g_hEvent);break;}}return 0;}

總結一下:事件對象要區分人工重設事件還是自動重設事件。如果是人工重設的事件對象得到通知,則等待該事件對象的所有線程均變為可調度線程;當一個自動重設的事件對象得到通知時,只有一個等待該事件對象的線程變為可調度線程,且作業系統會將該事件對象設為無訊號狀態。因此,當執行完受保護的代碼後,需要調用SetEvent將事件對象設為有訊號狀態。

下面介紹另一種線程同步的方法:關鍵程式碼片段。
關鍵程式碼片段又稱為臨界區,工作在使用者方式下。它是一小段代碼,但是在代碼執行之前,必須獨佔某些資源的存取權限。
我們先介紹與之先關的API函數:
使用InitializeCriticalSection初始化關鍵程式碼片段
使用EnterCriticalSection進入關鍵程式碼片段:
使用LeaveCriticalSection離開關鍵程式碼片段:
使用DeleteCriticalSection刪除關鍵程式碼片段,釋放資源
我們看一個例子:

#include <Windows.h>#include <stdio.h>//線程函式宣告DWORD WINAPI Thread1Proc(  LPVOID lpParameter);DWORD WINAPI Thread2Proc(  LPVOID lpParameter);//全域變數int tickets = 100;CRITICAL_SECTION g_cs;int main(){HANDLE hThread1;HANDLE hThread2;//初始化關鍵程式碼片段InitializeCriticalSection(&g_cs);//建立線程1hThread1 = CreateThread(NULL,//預設安全層級0,//預設棧大小Thread1Proc,//線程函數 NULL,//函數沒有參數0,//建立後直接運行NULL);//線程標識,不需要//建立線程2hThread2 = CreateThread(NULL,//預設安全層級0,//預設棧大小Thread2Proc,//線程函數 NULL,//函數沒有參數0,//建立後直接運行NULL);//線程標識,不需要//主線程休眠4秒Sleep(4000);//關閉線程控制代碼CloseHandle(hThread1);CloseHandle(hThread2);//關閉事件物件控點DeleteCriticalSection(&g_cs);return 0;}//線程1入口函數DWORD WINAPI Thread1Proc(  LPVOID lpParameter){while(TRUE){//進入關鍵程式碼片段前調用該函數判斷否能得到臨界區的使用權EnterCriticalSection(&g_cs);Sleep(1);if(tickets > 0){Sleep(1);printf("thread1 sell ticket : %d\n",tickets--);//訪問結束後釋放臨界區對象的使用權LeaveCriticalSection(&g_cs);Sleep(1);}else{LeaveCriticalSection(&g_cs);break;}}return 0;}//線程2入口函數DWORD WINAPI Thread2Proc(  LPVOID lpParameter){while(TRUE){//進入關鍵程式碼片段前調用該函數判斷否能得到臨界區的使用權EnterCriticalSection(&g_cs);Sleep(1);if(tickets > 0){Sleep(1);printf("thread2 sell ticket : %d\n",tickets--);//訪問結束後釋放臨界區對象的使用權LeaveCriticalSection(&g_cs);Sleep(1);}else{LeaveCriticalSection(&g_cs);break;}}return 0;}

在這個例子中,通過在放棄臨界區資源後,立即睡眠引起另一個線程被調用,導致兩個線程交替售票。
下面看一個多線程程式中常犯的一個錯誤-線程死結。死結產生的原因,舉個例子:線程1擁有臨界區資源A,正在等待臨界區資源B;而線程2擁有臨界區資源B,正在等待臨界區資源A。它倆各不相讓,結果誰也執行不了。我們看看程式:

#include <Windows.h>#include <stdio.h>//線程函式宣告DWORD WINAPI Thread1Proc(  LPVOID lpParameter);DWORD WINAPI Thread2Proc(  LPVOID lpParameter);//全域變數int tickets = 100;CRITICAL_SECTION g_csA;CRITICAL_SECTION g_csB;int main(){HANDLE hThread1;HANDLE hThread2;//初始化關鍵程式碼片段InitializeCriticalSection(&g_csA);InitializeCriticalSection(&g_csB);//建立線程1hThread1 = CreateThread(NULL,//預設安全層級0,//預設棧大小Thread1Proc,//線程函數 NULL,//函數沒有參數0,//建立後直接運行NULL);//線程標識,不需要//建立線程2hThread2 = CreateThread(NULL,//預設安全層級0,//預設棧大小Thread2Proc,//線程函數 NULL,//函數沒有參數0,//建立後直接運行NULL);//線程標識,不需要//關閉線程控制代碼//當不再引用這個控制代碼時,立即將其關閉,減少其引用計數CloseHandle(hThread1);CloseHandle(hThread2);//主線程休眠4秒Sleep(4000);//關閉事件物件控點DeleteCriticalSection(&g_csA);DeleteCriticalSection(&g_csB);return 0;}//線程1入口函數DWORD WINAPI Thread1Proc(  LPVOID lpParameter){while(TRUE){EnterCriticalSection(&g_csA);Sleep(1);EnterCriticalSection(&g_csB);if(tickets > 0){Sleep(1);printf("thread1 sell ticket : %d\n",tickets--);LeaveCriticalSection(&g_csB);LeaveCriticalSection(&g_csA);Sleep(1);}else{LeaveCriticalSection(&g_csB);LeaveCriticalSection(&g_csA);break;}}return 0;}//線程2入口函數DWORD WINAPI Thread2Proc(  LPVOID lpParameter){while(TRUE){EnterCriticalSection(&g_csB);Sleep(1);EnterCriticalSection(&g_csA);if(tickets > 0){Sleep(1);printf("thread2 sell ticket : %d\n",tickets--);LeaveCriticalSection(&g_csA);LeaveCriticalSection(&g_csB);Sleep(1);}else{LeaveCriticalSection(&g_csA);LeaveCriticalSection(&g_csB);break;}}return 0;}

在程式中,建立了兩個臨界區對象g_csA和g_csB。線程1中先嘗試擷取g_csA,擷取成功後休眠,線程2嘗試擷取g_csB,成功後休眠,切換回線程1,然後線程1試圖擷取g_csB,因為g_csB已經被線程2擷取,所以它線程1的擷取不會成功,一直等待,直到自己的時間片結束後,轉到線程2,線程2擷取g_csB後,試圖擷取g_csA,當然也不會成功,轉回線程1……這樣交替等待,直到主線程睡醒,執行完畢,程式結束。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.