C++四種同步手段
一,什麼是執行緒同步和互斥
同步就是協同步調,按預定的先後次序進行執行。如:你說完,我再說。這裡的同步千萬不要理解成那個同時進行,應是指協同、協助、互相配合。執行緒同步是指多執行緒通過特定的設定(如互斥量,事件物件,臨界區)來控制執行緒之間的執行順序(即所謂的同步)也可以說是線上程之間通過同步建立起執行順序的關係,如果沒有同步,那執行緒之間是各自執行各自的!
執行緒互斥是指對於共享的程序系統資源,在各單個執行緒訪問時的排它性。當有若干個執行緒都要使用某一共享資源時,任何時刻最多隻允許一個執行緒去使用,其它要使用該資源的執行緒必須等待,直到佔用資源者釋放該資源。執行緒互斥可以看成是一種特殊的執行緒同步(下文統稱為同步)。
文末收錄了一篇關於併發 並行 同步 非同步 多執行緒的區別(這是原文地址,尊重原創)
二,執行緒同步的方式和機制
臨界區(Critical Section)、互斥物件(Mutex):主要用於互斥控制;都具有擁有權的控制方法,只有擁有該物件的執行緒才能執行任務,所以擁有,執行完任務後一定要釋放該物件。
訊號量(Semaphore)、事件物件(Event):事件物件是以通知的方式進行控制,主要用於同步控制!
1、臨界區:通過對多執行緒的序列化來訪問公共資源或一段程式碼,速度快,適合控制資料訪問。在任意時刻只允許一個執行緒對共享資源進行訪問,如果有多個執行緒試圖訪問公共資源,那麼在有一個執行緒進入後,其他試圖訪問公共資源的執行緒將被掛起,並一直等到進入臨界區的執行緒離開,臨界區在被釋放後,其他執行緒才可以搶佔。它並不是核心物件,不是屬於作業系統維護的,而是屬於程序維護的。
總結下關鍵段:
1)關鍵段共初始化化、銷燬、進入和離開關鍵區域四個函式。
2)關鍵段可以解決執行緒的互斥問題,但因為具有“執行緒所有權”,所以無法解決同步問題。
3)推薦關鍵段與旋轉鎖配合使用。
2、互斥物件:互斥物件和臨界區很像,採用互斥物件機制,只有擁有互斥物件的執行緒才有訪問公共資源的許可權。因為互斥物件只有一個,所以能保證公共資源不會同時被多個執行緒同時訪問。當前擁有互斥物件的執行緒處理完任務後必須將執行緒交出,以便其他執行緒訪問該資源。
總結下互斥量Mutex:
1)互斥量是核心物件,它與關鍵段都有“執行緒所有權”所以不能用於執行緒的同步。
2)互斥量能夠用於多個程序之間執行緒互斥問題,並且能完美的解決某程序意外終止所造成的“遺棄”問題。
3、訊號量:訊號量也是核心物件。它允許多個執行緒在同一時刻訪問同一資源,但是需要限制在同一時刻訪問此資源的最大執行緒數目
在用CreateSemaphore()建立訊號量時即要同時指出允許的最大資源計數和當前可用資源計數。一般是將當前可用資源計數設定為最 大資源計數,每增加一個執行緒對共享資源的訪問,當前可用資源計數就會減1 ,只要當前可用資源計數是大於0 的,就可以發出訊號量訊號。但是當前可用計數減小 到0 時則說明當前佔用資源的執行緒數已經達到了所允許的最大數目,不能在允許其他執行緒的進入,此時的訊號量訊號將無法發出。執行緒在處理完共享資源後,應在離 開的同時通過ReleaseSemaphore ()函式將當前可用資源計數加1 。在任何時候當前可用資源計數決不可能大於最大資源計數。
4、事件物件: 通過通知操作的方式來保持執行緒的同步,還可以方便實現對多個執行緒的優先順序比較的操作
總結下事件Event
1)事件是核心物件,事件分為手動置位事件和自動置位事件。事件Event內部它包含一個使用計數(所有核心物件都有),一個布林值表示是手動置位事件還是自動置位事件,另一個布林值用來表示事件有無觸發。
2)事件可以由SetEvent()來觸發,由ResetEvent()來設成未觸發。還可以由PulseEvent()來發出一個事件脈衝。
3)事件可以解決執行緒間同步問題,因此也能解決互斥問題。
三,四種執行緒同步控制方式
1,互斥物件
第一個CreateMutex
函式功能:建立互斥量(注意與事件Event的建立函式對比)
函式原型:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner,
LPCTSTR lpName
);
函式說明:
第一個引數表示安全控制,一般直接傳入NULL。
第二個引數用來確定互斥量的初始擁有者。如果傳入TRUE表示互斥量物件內部會記錄建立它的執行緒的執行緒ID號並將遞迴計數設定為1,由於該執行緒ID非零,所以互斥量處於未觸發狀態。如果傳入FALSE,那麼互斥量物件內部的執行緒ID號將設定為NULL,遞迴計數設定為0,這意味互斥量不為任何執行緒佔用,處於觸發狀態。
第三個引數用來設定互斥量的名稱,在多個程序中的執行緒就是通過名稱來確保它們訪問的是同一個互斥量。
函式訪問值:
成功返回一個表示互斥量的控制代碼,失敗返回NULL。
第二個開啟互斥量
函式原型:
HANDLE OpenMutex(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName //名稱
);
函式說明:
第一個引數表示訪問許可權,對互斥量一般傳入MUTEX_ALL_ACCESS。詳細解釋可以檢視MSDN文件。
第二個引數表示互斥量控制代碼繼承性,一般傳入TRUE即可。
第三個引數表示名稱。某一個程序中的執行緒建立互斥量後,其它程序中的執行緒就可以通過這個函式來找到這個互斥量。
函式訪問值:
成功返回一個表示互斥量的控制代碼,失敗返回NULL。
第三個觸發互斥量
函式原型:
BOOL ReleaseMutex (HANDLE hMutex)
函式說明:
訪問互斥資源前應該要呼叫等待函式,結束訪問時就要呼叫ReleaseMutex()來表示自己已經結束訪問,其它執行緒可以開始訪問了。
最後一個清理互斥量
由於互斥量是核心物件,因此使用CloseHandle()就可以(這一點所有核心物件都一樣)。
互斥物件的形象比喻:
首先我們需要建立CreateMutex一把互斥物件,我們可以指明當前執行緒是否擁有它,互斥物件完全就像一把鑰匙一樣,我們用WaitForSignalObject來等待這把鑰匙,但是這把鑰匙被等到並且使用後必須釋放-----ReleaseMutex ,不然別人永遠無法等到。這樣從等待到釋放中間的程式碼段永遠都是隻有一個執行緒在執行,也就形成了互斥控制。當然互斥物件的控制代碼是要關閉的CloseHandle。
結合下面的程式理解簡直輕鬆。
互斥物件模擬火車售票的過程
(對共同資源的互斥控制)
#include "windows.h"
#include "iostream"
using namespace std;
DWORD WINAPI FunProc1(LPVOID lpParameter);
DWORD WINAPI FunProc2(LPVOID lpParameter);
int ticket = 100;
HANDLE hMutex; //定義互斥物件
int main()
{
HANDLE hThread1;
HANDLE hThread2;
hMutex = CreateMutex(NULL, FALSE, NULL); //建立互斥物件,並且該執行緒不擁有他(因為第二個引數為false)
hThread1 = CreateThread(NULL, 0, FunProc1, NULL, 0, NULL);
hThread2 = CreateThread(NULL, 0, FunProc2, NULL, 0, NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
Sleep(1000);// 讓主執行緒睡眠1秒 ,不然子執行緒來不及執行
return 0;
}
DWORD WINAPI FunProc1(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(hMutex, INFINITE); //申請互斥物件的所有權(申請鑰匙,得到鑰匙)
if (ticket>0)
{
Sleep(1);
cout << "ticket 1:" << ticket-- << endl;
}
else
break;
ReleaseMutex(hMutex);//釋放互斥物件的所有權(放棄鑰匙,不再擁有)
}
return 0;
}
DWORD WINAPI FunProc2(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(hMutex, INFINITE);
if (ticket>0)
{
Sleep(1);
cout << "ticket 2:" << ticket-- << endl;
}
else
break;
ReleaseMutex(hMutex);
}
return 0;
}
分析程式互斥過程
建立互斥物件後,第二各引數是false表示當前主執行緒不擁有互斥物件,此時互斥物件沒人擁有,所以誰都可以獲取他,然後主執行緒空閒1秒(子執行緒才時間執行,不然主執行緒一下就退出了),在這一秒內第一個執行緒waitforsingleobject得到了互斥物件(不然無法執行),開始賣火車票,然後釋放互斥物件的擁有權,接著第二個執行緒通過waitforsingleobject得到了互斥物件,所以他又可以執行售賣火車票,接著釋放互斥物件...如此往返!知道火車票賣完,while迴圈退出。
注意:雖然改程式執行結果是某一條執行緒執行完然後第二條執行緒執行,如此往復,但是這不是同步,因為我們沒法控制到底一開始是誰先執行,(我們只是控制了輪流次序)。
2,事件物件
第一個 CreateEvent
函式功能:建立事件
函式原型:
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,
LPCTSTR lpName
);
函式說明:
第一個引數表示安全控制,一般直接傳入NULL。
第二個引數確定事件是手動置位還是自動置位,傳入TRUE表示手動置位,傳入FALSE表示自動置位。如果為自動置位,則對該事件呼叫WaitForSingleObject()後會自動呼叫ResetEvent()使事件變成未觸發狀態。
第三個引數表示事件的初始狀態,傳入TRUR表示已觸發。
第四個引數表示事件的名稱,傳入NULL表示匿名事件。
第二個 OpenEvent
函式功能:根據名稱獲得一個事件控制代碼。
函式原型:
HANDLE OpenEvent(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName //名稱
);
函式說明:
第一個引數表示訪問許可權,對事件一般傳入EVENT_ALL_ACCESS。詳細解釋可以檢視MSDN文件。
第二個引數表示事件控制代碼繼承性,一般傳入TRUE即可。
第三個引數表示名稱,不同程序中的各執行緒可以通過名稱來確保它們訪問同一個事件。
第三個SetEvent
函式功能:觸發事件
函式原型:BOOL SetEvent(HANDLE hEvent);
函式說明:每次觸發後,必有一個或多個處於等待狀態下的執行緒變成可排程狀態。
第四個ResetEvent
函式功能:將事件設為末觸發
函式原型:BOOLResetEvent(HANDLEhEvent);
最後一個事件的清理與銷燬
由於事件是核心物件,因此使用CloseHandle()就可以完成清理與銷燬了。
事件物件的形象比喻:
首先我們需要建立CreateEvent一個事件物件,它的使用方式是觸發方式,要想被WaitForSingleObject等待到該事件物件必須是有訊號的,事件要想有訊號可以用SetEvent手動置為有訊號,要想事件物件無訊號可以使用ResetEvent(或者在建立事件物件時就宣告該事件物件WaitForSingleObject後自動置為無訊號,見上面CreateEvent第二個引數),打個小小比方,手動置位事件相當於教室門,教室門一旦開啟(被觸發),所以有人都可以進入直到老師去關上教室門(事件變成未觸發)。自動置位事件就相當於醫院裡拍X光的房間門,門開啟後只能進入一個人,這個人進去後會將門關上,其它人不能進入除非門重新被開啟(事件重新被觸發)。當然事件物件的控制代碼是要關閉的CloseHandle。
結合下面的程式理解簡直輕鬆。
事件物件模擬火車售票的過程
(利用通知的方式對共同資源的互斥控制)
#include "windows.h"
#include "iostream"
using namespace std;
DWORD WINAPI FunProc1(LPVOID lpParameter);
DWORD WINAPI FunProc2(LPVOID lpParameter);
int ticket = 100;
HANDLE g_hEvent;
void main()
{
HANDLE hThread1;
HANDLE hThread2;
g_hEvent = CreateEvent(NULL, FALSE, TRUE, NULL); //建立事件物件
//SetEvent(g_hEvent); //設定為有訊號(放棄鑰匙)
hThread1 = CreateThread(NULL, 0, FunProc1, NULL, 0, NULL);
hThread2 = CreateThread(NULL, 0, FunProc2, NULL, 0, NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
Sleep(1000); // 讓主執行緒睡眠1秒
CloseHandle(g_hEvent);
}
DWORD WINAPI FunProc1(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(g_hEvent, INFINITE); //申請事件物件
//ResetEvent(g_hEvent); //重置為無訊號(申請鑰匙,得帶鑰匙)
if (ticket>0)
{
Sleep(1);
cout << "ticket 1:" << ticket-- << endl;
SetEvent(g_hEvent); //設定為有訊號(放棄鑰匙,不再擁有)
}
else
{
SetEvent(g_hEvent); //設定為有訊號
break;
}
}
return 0;
}
DWORD WINAPI FunProc2(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(g_hEvent, INFINITE);
//ResetEvent(g_hEvent);
if (ticket>0)
{
Sleep(1);
cout << "ticket 2:" << ticket-- << endl;
SetEvent(g_hEvent); //設定為有訊號
}
else
{
SetEvent(g_hEvent); //設定為有訊號
break;
}
}
return 0;
}
同步過程分析
g_hEvent=CreateEvent(NULL,FALSE,TRUE,NULL);的意義(順序解釋):預設安全性,自動重置事件,初始時該事件物件就有訊號。執行順序:開始的時候事件物件具有訊號,當第一個執行緒申請獲得事件物件後,進入if語句執行緒1會暫停1毫秒,於是第二根執行緒執行,因為此時g_hEvent已經無訊號故無法申請並執行下面的程式,此時第一個執行緒睡醒開始執行自己的任務然後設定物件為有訊號(可以被其他執行緒申請),於是第二個執行緒申請得到事件物件................與此往復!直到退出迴圈。
注意:該程式實現的不是的同步控制!但是他可以實現同步,見下面!
下面這種事件通知方式就是嚴格的同步方式(先讓執行緒1執行,再.....),兩個事件物件進行同步控制:
#include "windows.h"
#include "iostream"
using namespace std;
DWORD WINAPI FunProc1(LPVOID lpParameter);
DWORD WINAPI FunProc2(LPVOID lpParameter);
int ticket = 100;
HANDLE g_hEvent1,g_hEvent2;
void main()
{
HANDLE hThread1;
HANDLE hThread2;
g_hEvent1 = CreateEvent(NULL, FALSE, TRUE, NULL); //建立事件物件1,自動重置事件,已觸發
g_hEvent2 = CreateEvent(NULL, FALSE, FALSE, NULL); //建立事件物件2,自動重置事件,沒觸發
//SetEvent(g_hEvent); //設定為有訊號(放棄鑰匙)
hThread1 = CreateThread(NULL, 0, FunProc1, NULL, 0, NULL);
hThread2 = CreateThread(NULL, 0, FunProc2, NULL, 0, NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
Sleep(1000); // 讓主執行緒睡眠1秒
CloseHandle(g_hEvent1);
CloseHandle(g_hEvent2);
getchar();
}
DWORD WINAPI FunProc1(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(g_hEvent1, INFINITE); //申請有訊號的事件物件
//ResetEvent(g_hEvent); //重置為無訊號(申請鑰匙,得帶鑰匙)
if (ticket>0)
{
Sleep(1);
cout << "ticket 1:" << ticket-- << endl;
SetEvent(g_hEvent2); //設定為有訊號(放棄鑰匙,不再擁有)
}
else
{
SetEvent(g_hEvent2); //設定為有訊號,讓執行緒2執行
break;
}
}
return 0;
}
DWORD WINAPI FunProc2(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(g_hEvent2, INFINITE);
//ResetEvent(g_hEvent);
if (ticket>0)
{
Sleep(1);
cout << "ticket 2:" << ticket-- << endl;
SetEvent(g_hEvent1); //讓執行緒1執行
}
else
{
SetEvent(g_hEvent1);
break;
}
}
return 0;
}
3,臨界區(關鍵程式碼段)
函式功能:初始化
函式原型:
void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
函式說明:定義關鍵段變數後必須先初始化。
函式功能:銷燬
函式原型:
void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
函式說明:用完之後記得銷燬。
函式功能:進入關鍵區域
函式原型:
void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
函式說明:系統保證各執行緒互斥的進入關鍵區域。
函式功能:離開關關鍵區域
函式原型:
void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
臨界區模擬火車售票的過程
新增程式碼如下
#include "windows.h"
#include "iostream"
using namespace std;
DWORD WINAPI FunProc1(LPVOID lpParameter);
DWORD WINAPI FunProc2(LPVOID lpParameter);
int ticket = 100;
CRITICAL_SECTION g_cs; //定義臨界區
void main()
{
HANDLE hThread1;
HANDLE hThread2;
InitializeCriticalSection(&g_cs); //必須先初始化臨界區
hThread1 = CreateThread(NULL, 0, FunProc1, NULL, 0, NULL);
hThread2 = CreateThread(NULL, 0, FunProc2, NULL, 0, NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
Sleep(1000);// 讓主執行緒睡眠1秒
DeleteCriticalSection(&g_cs); //刪除臨界區
}
DWORD WINAPI FunProc1(LPVOID lpParameter)
{
while (TRUE)
{
EnterCriticalSection(&g_cs);//進入臨界區(申請鑰匙,得到鑰匙)
Sleep(1);
if (ticket>0)
{
Sleep(1);
cout << "ticket 1:" << ticket-- << endl;
LeaveCriticalSection(&g_cs); //離開(放棄鑰匙,不再擁有)
}
else
{
LeaveCriticalSection(&g_cs); //離開
break;
}
}
return 0;
}
DWORD WINAPI FunProc2(LPVOID lpParameter)
{
while (TRUE)
{
EnterCriticalSection(&g_cs);
if (ticket>0)
{
Sleep(1);
cout << "ticket 2:" << ticket-- << endl;
LeaveCriticalSection(&g_cs);
}
else
{
LeaveCriticalSection(&g_cs);
break;
}
}
return 0;
}
4,訊號量
訊號量Semaphore常用有三個函式,使用很方便。下面是這幾個函式的原型和使用說明。
HANDLE CreateSemaphore(
LPSECURITY ATTRIBUTES lpSemaphoreAttributes, //安全屬性
LONG lInitialCount, //訊號量物件的初始值
LONG lMaximumCount, //訊號量的最大值
LPCTSTR lpName //訊號量名
);
引數說明:
(1)lpSemaphoreAttributes:指定安全屬性,為NULL時,訊號量得到一個
預設的安全描述符。
(2) lInitialCount:指定訊號量物件的初始值。該值必須大於等於0,小於等於lMaximumCount。當其值大於0時,訊號量被喚醒。當該函式釋放了一個等待該訊號量的執行緒時,lInitialCount值減1,當呼叫函式ReleaseSemaphore()時,按其指定的數量加一個值。
(3) lMaximumCount:指出該訊號量的最大值,該值必須大於0。
(4) lpName:給出訊號量的名字。
返回值:
訊號量建立成功,將返回該訊號量的控制代碼。如果給出的訊號量名是系統已經存在的訊號量,將返回這個已存在訊號量的控制代碼。如果失敗,系統返回NULL,可以呼叫函式GetLastError()查詢失敗的原因
第二個 OpenSemaphore
函式功能:開啟訊號量
函式原型:
HANDLE OpenSemaphore(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName
);
函式說明:
第一個引數表示訪問許可權,對一般傳入SEMAPHORE_ALL_ACCESS。詳細解釋可以檢視MSDN文件。
第二個引數表示訊號量控制代碼繼承性,一般傳入TRUE即可。
第三個引數表示名稱,不同程序中的各執行緒可以通過名稱來確保它們訪問同一個訊號量。
第三個 ReleaseSemaphore
函式功能:遞增訊號量的當前資源計數
函式原型:
BOOL ReleaseSemaphore(
HANDLE hSemaphore,
LONG lReleaseCount,
LPLONG lpPreviousCount
);
函式說明:
第一個引數是訊號量的控制代碼。
第二個引數表示增加個數,必須大於0且不超過最大資源數量。
第三個引數可以用來傳出先前的資源計數,設為NULL表示不需要傳出。
注意:當前資源數量大於0,表示訊號量處於觸發,等於0表示資源已經耗盡故訊號量處於末觸發。在對訊號量呼叫等待函式時,等待函式會檢查訊號量的當前資源計數,如果大於0(即訊號量處於觸發狀態),減1後返回讓呼叫執行緒繼續執行。一個執行緒可以多次呼叫等待函式來減小訊號量。
最後一個 訊號量的清理與銷燬
由於訊號量是核心物件,因此使用CloseHandle()就可以完成清理與銷燬了。
訊號量超形象解釋:
以一個停車場的運作為例。簡單起見,假設停車場只有三個車位(共有資源),一開始三個車位都是空的。這時如果同時來了五輛車(執行緒),看門人(訊號量)允許其中三輛(執行緒)直接進入,然後放下車攔,剩下的車則必須在入口等待,此後來的車也都不得不在入口處等待。這時,有一輛車(執行緒)離開停車場,看門人(訊號量)得知後,開啟車攔,放入外面的一輛進去,如果又離開兩輛,則又可以放入兩輛,如此往復。
抽象的來講,訊號量的特性如下:訊號量是一個非負整數(車位數),所有通過它的執行緒/程序(車輛)都會將該整數減一(通過它使得資源被使用了1個),當該整數值為零時,所有試圖通過它的執行緒(車輛)都將處於等待狀態。在訊號量上我們定義兩種操作: Wait(等待函式) 和 Release(釋放函式)。當一個執行緒呼叫Wait操作時,它要麼得到資源然後將訊號量減一,要麼一直等下去(指放入阻塞佇列),直到訊號量大於等於一時。Release(釋放)對應於車輛離開停車場,該操作之所以叫做“釋放”是因為釋放了由訊號量守護的資源(車位)。
參考學習例子:
#include <stdio.h>
#include <process.h>
#include <windows.h>
long g_nNum;
unsigned int __stdcall Fun(void *pPM);
const int THREAD_NUM = 10;
//訊號量與關鍵段
HANDLE g_hThreadParameter;//用訊號量處理主執行緒與子執行緒的同步
CRITICAL_SECTION g_csThreadCode;//關鍵段來處理各子執行緒間的互斥.對公有資源進行互斥訪問,公有資源g_nNum
int main()
{
//初始化訊號量和關鍵段
g_hThreadParameter = CreateSemaphore(NULL, 0, 1, NULL);//第二個引數表示初始資源數量。當前0個資源,最大允許1個同時訪問
InitializeCriticalSection(&g_csThreadCode);
HANDLE handle[THREAD_NUM];
g_nNum = 0;
int i = 0;
while (i < THREAD_NUM)
{
handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
Sleep(1);
WaitForSingleObject(g_hThreadParameter, INFINITE);//依次使執行緒進入等待狀態(阻塞佇列)。參見《windows核心程式設計第五版》
++i;
}
WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
DeleteCriticalSection(&g_csThreadCode);
//銷燬訊號量和關鍵段
CloseHandle(g_hThreadParameter);
for (i = 0; i < THREAD_NUM; i++)
CloseHandle(handle[i]);
getchar();
return 0;
}
unsigned int __stdcall Fun(void *pPM)
{
int nThreadNum = *(int *)pPM;
ReleaseSemaphore(g_hThreadParameter, 1, NULL);//訊號量的當前資源+1,第二個引數表示增加個數,必須大於0且不超過最大資源數量。
Sleep(50);//some work should to do
EnterCriticalSection(&g_csThreadCode);
++g_nNum;
Sleep(10);//some work should to do
printf("執行緒編號為%d 全域性資源值為%d\n", nThreadNum, g_nNum);
LeaveCriticalSection(&g_csThreadCode);
return 0;
}
收錄博文:
1. 併發:在作業系統中,是指一個時間段中有幾個程式都處於已啟動執行到執行完畢之間,且這幾個程式都是在同一個處理機上執行。其中兩種併發關係分別是同步和互斥
2. 互斥:程序間相互排斥的使用臨界資源的現象,就叫互斥。
3. 同步:程序之間的關係不是相互排斥臨界資源的關係,而是相互依賴的關係。進一步的說明:就是前一個程序的輸出作為後一個程序的輸入,當第一個程序沒有輸出時第二個程序必須等待。具有同步關係的一組併發程序相互發送的資訊稱為訊息或事件。其中併發又有偽併發和真併發,偽併發是指單核處理器的併發,真併發是指多核處理器的併發。
4. 並行:在單處理器中多道程式設計系統中,程序被交替執行,表現出一種併發的外部特種;在多處理器系統中,程序不僅可以交替執行,而且可以重疊執行。在多處理器上的程式才可實現並行處理。從而可知,並行是針對多處理器而言的。並行是同時發生的多個併發事件,具有併發的含義,但併發不一定並行,也亦是說併發事件之間不一定要同一時刻發生。
5. 多執行緒:多執行緒是程式設計的邏輯層概念,它是程序中併發執行的一段程式碼。多執行緒可以實現執行緒間的切換執行。
6. 非同步:非同步和同步是相對的,同步就是順序執行,執行完一個再執行下一個,需要等待、協調執行。非同步就是彼此獨立,在等待某事件的過程中繼續做自己的事,不需要等待這一事件完成後再工作。執行緒就是實現非同步的一個方式。非同步是讓呼叫方法的主執行緒不需要同步等待另一執行緒的完成,從而可以讓主執行緒幹其它的事情。
非同步和多執行緒並不是一個同等關係,非同步是最終目的,多執行緒只是我們實現非同步的一種手段。非同步是當一個呼叫請求傳送給被呼叫者,而呼叫者不用等待其結果的返回而可以做其它的事情。實現非同步可以採用多執行緒技術或則交給另外的程序來處理。
為了對以上概念的更好理解舉一個簡單例子,
假設我要做 燒開水,舉槓鈴100下, 洗衣服 3件事情。
燒開水 這件事情, 我要做的事情為, 準備燒開水 1分鐘, 等開水燒開 8 分鐘 , 關掉燒水機 1分鐘
舉槓鈴100下 我要做的事情為, 舉槓鈴100下 10分鐘
洗衣服 我要做的事情為, 準備洗衣服 1分鐘, 等開水燒開 5 分鐘 , 關掉洗衣機 1分鐘
單核情況下
同步的完成,我需要做的時間為 1+ 8 +1 + 10 + 1+ 5 +1 = 27 分
如果非同步,就是在等的時候,我可以切換去做別的事情
準備燒開水(1) + 準備洗衣服(1) + 舉50下槓鈴 (5)分鐘+ 關洗衣機 1分鐘 + 舉槓鈴20下 (2)分鐘+ 關燒水機 1分鐘 + 舉30下槓鈴(3)分鐘
1+1+5+1+2+1+3 =14 分鐘
雙核 非同步 並行
核1 準備燒開水 1分鐘+ 舉槓鈴50下(5)分鐘+ 等待3分鐘 + 關掉燒水機 1分鐘
核2 準備洗衣服 1分鐘+ 舉槓鈴50下(5)分鐘+ 關掉洗衣機 1分鐘 + 等待3分鐘
其實只花了 1+5+3+1 = 10分鐘
其中還有雙核都等待了3分鐘
雙核 非同步 非並行
核1 舉槓鈴100下(10)分鐘
核2 準備燒開水 1分鐘+ 準備洗衣服 1分鐘+ 等待5 分鐘+ + 關掉燒水機 1分鐘 + 等待 1 分鐘 + 關掉洗衣機 1分鐘
其實只花了 1+5+3+1 = 10分鐘
多執行緒的做法
單核下
執行緒1 準備燒開水 1分鐘, 等開水燒開 8 分鐘 , 關掉燒水機 1分鐘
執行緒2 舉槓鈴100下 10分鐘
執行緒3 準備洗衣服 1分鐘, 等開水燒開 5 分鐘 , 關掉洗衣機 1分鐘
cpu 可能這麼切換 最理想的切換方式
執行緒1 準備燒開水1 sleep 1 sleep 5 sleep 1 sleep 2 關開水 1分鐘 exit
執行緒2 sleep 1 sleep 1 舉槓鈴50 5分鐘 sleep 1 舉槓鈴20 2分鐘 sleep1 舉槓鈴30下 3分鐘
執行緒3 sleep 1 準備洗衣服1 分鐘 sleep 5 關洗衣機1分鐘 exit
最後使用了 14分鐘 和非同步是一樣的。
但是實際上是不一樣的,因為執行緒不會按照我們設想的去跑, 如果執行緒2 舉槓鈴先跑,整個流程的速度就下來了。
非同步和同步的區別, 在io等待的時候,同步不會切走,浪費了時間。
如果都是獨佔cpu 的業務, 比如舉槓鈴的業務, 在單核情況下 多線和單線 沒有區別。
多執行緒的好處,比較容易的實現了 非同步切換的思想, 因為非同步的程式很難寫的。多執行緒本身程還是以同步完成,但是應該說
比效率是比不上非同步的。 而且多線很容易寫, 相對效率也高。
多核的好處,就是可以同時做事情, 這個和單核完全不一樣的。
---------------------
作者:EbowTang
來源:CSDN
原文:https://blog.csdn.net/ebowtang/article/details/29905309