【轉】同步機制及windows同步函數的使用
作者:simahao
原文連結:http://blog.csdn.net/simahao/archive/2005/07/15/425420.aspx
本篇文章適合比較熟悉多線程並且想學習線程同步的讀者。
最近由於使用多線程,不可避免的要用到線程之間的同步,對一些常用的windows 中同步函數和機制有了一些初步的瞭解,並且寫了一些小例子來驗證,當然其中難免有錯誤和疏漏之處,希望高手能給我這個小鳥指出不足之處,非常感謝。
目錄
一 臨界區
二 互斥體
三 事件
四 訊號量
五 附錄
一 臨界區
臨界區的使用線上程同步中應該算是比較簡單,說它簡單還是說它同後面講到的其它方法相比更容易理解。舉個簡單的例子:比如說有一個全域變數(公用資源)兩個線程都會對它進行寫操作和讀操作,如果我們在這裡不加以控制,會產生意想不到的結果。假設線程A正在把全域變數加1然後列印在螢幕上,但是這時切換到線程B,線程B又把全域變數加1然後又切換到線程A,這時候線程A列印的結果就不是程式想要的結果,也就產生了錯誤。解決的辦法就是設定一個地區,讓線程A在操縱全域變數的時候進行加鎖,線程B如果想操縱這個全域變數就要等待線程A釋放這個鎖,這個也就是臨界區的概念。
使用方法:
CRITICAL_SECTION cs;
InitializeCriticalSection(&cs);
EnterCriticalSection(&cs);
...
LeaveCriticalSection(&cs);
DeleteCriticalSection(&cs);
#include "stdafx.h"
#include <windows.h>
#include <process.h>
#include <iostream>
using namespace std;
/****************************************************************
*在使用臨界區的時候要注意,每一個共用資源就有一個CRITICAL_SECTION
*如果要一次訪問多個共用變數,各個線程要保證訪問的順序一致,如果不
*一致,很可能發生死結。例如:
* thread one:
* EnterCriticalSection(&c1)
* EnterCriticalSection(&c2)
* ...
* Leave...
* Leave...
*
* thread two:
* EnterCriticalSection(&c2);
* EnterCriticalSection(&c1);
* ...
* Leave...
* Leave...
*這樣的情況就會發生死結,應該讓線程2進入臨界區的順序同線程1相同
****************************************************************/
const int MAX_THREADNUMS = 4; //產生線程數目
CRITICAL_SECTION cs; //臨界區
HANDLE event[MAX_THREADNUMS]; //儲存createevent的返回handle
int critical_value = 0; //共用資源
UINT WINAPI ThreadFunc(void* arg)
{
int thread = (int)arg;
for (int i = 0; i < 5; i++)
{
EnterCriticalSection(&cs);
cout << "thread " << thread << " ";
critical_value++;
cout << "critical_value = " << critical_value << endl;
LeaveCriticalSection(&cs);
}
SetEvent(event[thread]);
return 1;
}
int main(int argc, char* argv[])
{
cout << "this is a critical_section test program" << endl;
HANDLE hThread;
UINT uThreadID;
DWORD dwWaitRet = 0;
InitializeCriticalSection(&cs);
for (int i = 0; i < MAX_THREADNUMS; i++)
{
event[i] = CreateEvent(NULL, TRUE, FALSE, "");
if (event[i] == NULL)
{
cout << "create event " << i << " failed with code: "
<< GetLastError() << endl;
continue;
}
hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc,
(void*)i, 0, &uThreadID);
if (hThread == 0)
{
cout << "begin thread " << i << " failed with code: "
<< GetLastError() << endl;
continue;
}
CloseHandle(hThread);
}
//等待所有線程完成
dwWaitRet = WaitForMultipleObjects(MAX_THREADNUMS, event, TRUE, INFINITE);
switch(dwWaitRet)
{
case WAIT_OBJECT_0:
cout << "all the sub thread has exit!" << endl;
break;
default:
cout << "wait for all the thread failed with code:" << GetLastError() << endl;
break;
}
DeleteCriticalSection(&cs);
for (int k = 0; k < MAX_THREADNUMS; k++)
{
CloseHandle(event[k]);
}
return 0;
}
二 互斥體
windows api中提供了一個互斥體,功能上要比臨界區強大。也許你要問,這個東東和臨界區有什麼區別,為什麼強大?它們有以下幾點不一致:
1.critical section是局部對象,而mutex是核心對象。因此像waitforsingleobject是不可以等待臨界區的。
2.critical section是快速高效的,而mutex同其相比要慢很多
3.critical section使用範圍是單一進程中的各個線程,而mutex由於可以有一個名字,因此它是可以應用於不同的進程,當然也可以應用於同一個進程中的不同線程。
4.critical section 無法檢測到是否被某一個線程釋放,而mutex在某一個線程結束之後會產生一個abandoned的資訊。同時mutex只能被擁有它的線程釋放。下面舉兩個應用mutex的例子,一個是程式只能運行一個執行個體,也就是說同一個程式如果已經運行了,就不能再運行了;另一個是關於非常經典的哲學家吃飯問題的例子。
程式運行單個執行個體:
#include "stdafx.h"
#include <windows.h>
#include <process.h>
#include <iostream>
using namespace std;
//當輸入s或者c時候結束程式
void PrintInfo(HANDLE& h, char t)
{
char c;
while (1)
{
cin >> c;
if (c == t)
{
ReleaseMutex(h);
CloseHandle(h);
break;
}
Sleep(100);
}
}
int main(int argc, char* argv[])
{
//建立mutex,當已經程式發現已經有這個mutex時候,就相當於openmutex
HANDLE hHandle = CreateMutex(NULL, FALSE, "mutex_test");
if (GetLastError() == ERROR_ALREADY_EXISTS)
{
cout << "you had run this program!" << endl;
cout << "input c to close this window" << endl;
PrintInfo(hHandle, 'c');
return 1;
}
cout << "program run!" << endl;
cout << "input s to exit program" <<endl;
PrintInfo(hHandle, 's');
return 1;
}
哲學家吃飯問題:
const int PHILOSOPHERS = 5; //哲學家人數
const int TIME_EATING = 50; //吃飯需要的時間 毫秒
HANDLE event[PHILOSOPHERS]; //主線程同背景工作執行緒保持同步的控制代碼數組
HANDLE mutex[PHILOSOPHERS]; //mutex數組,這裡相當於公用資源筷子
CRITICAL_SECTION cs; //控制列印的臨界區變數
UINT WINAPI ThreadFunc(void* arg)
{
int num = (int)arg;
DWORD ret = 0;
while (1)
{
ret = WaitForMultipleObjects(2, mutex, TRUE, 1000);
if (ret == WAIT_TIMEOUT)
{
Sleep(100);
continue;
}
EnterCriticalSection(&cs);
cout << "philosopher " << num << " eatting" << endl;
LeaveCriticalSection(&cs);
Sleep(TIME_EATING);
break;
}
//設定時間為有訊號
SetEvent(event[num]);
return 1;
}
int main(int argc, char* argv[])
{
HANDLE hThread;
InitializeCriticalSection(&cs);
//迴圈建立線程
for (int i = 0; i < PHILOSOPHERS; i++)
{
mutex[i] = CreateMutex(NULL, FALSE, "");
event[i] = CreateEvent(NULL, TRUE, FALSE, "");
hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, (void*)i, 0, NULL);
if (hThread == 0)
{
cout << "create thread " << i << "failed with code: "
<< GetLastError() << endl;
DeleteCriticalSection(&cs);
return -1;
}
CloseHandle(hThread);
}
//等待所有的哲學家吃飯結束
DWORD ret = WaitForMultipleObjects(PHILOSOPHERS, event, TRUE, INFINITE);
if (ret == WAIT_OBJECT_0)
{
cout << "all the philosophers had a dinner!" << endl;
}
else
{
cout << "WaitForMultipleObjects failed with code: " << GetLastError() << endl;
}
DeleteCriticalSection(&cs);
for (int j = 0; j < PHILOSOPHERS; j++)
{
CloseHandle(mutex[j]);
}
return 1;
}
三 事件
事件對象的特點是它可以應用在重疊I/O(overlapped I/0)上,比如說socket編程中有兩種模型,一種是重疊I/0,一種是完成連接埠都是可以使用事件同步。它也是核心對象,因此可以被waitforsingleobje這些函數等待;事件可以有名字,因此可以被其他進程開啟。我在前幾個例子當中其實已經使用到event了,在這裡就不多說了,可以參考前一個例子。
四 訊號量
semaphore的概念理解起來可能要比mutex還難, 我先簡單說一下建立訊號量的函數,因為我在開始使用的時候沒有很快弄清楚,可能現在還有理解不對的地方,如果有錯誤還是請大俠多多指教。
CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // SD
LONG lInitialCount, // initial count
LONG lMaximumCount, // maximum count
LPCTSTR lpName // object name
)
第一個參數是安全性,可以使用預設的安全性選項NULL;第二個和第三個參數是兩個long型的數值,它們表示什麼含義呢?lMaxinumCount表示訊號量的最大值,必須要大於零。比如是5就表示可以有5個進程或者線程使用,如果第六個進程或者線程想使用的話就必須進入等待隊列等待有進程或者線程釋放資源。lInitalCount表示訊號量的初始值,應該大於或者等於零小於等於lMaximumCount。如果lInitialCount = 0 && lMaximumCount == 5,那麼就表示當前資源已經全部被使用,如果再有進程或者線程想使用的話,訊號量就會變成-1,該進程或者線程進入等待隊列,直到有進程或者線程執行ReleaseMutex;如果lInitialCount = 5 && lMaximumCount == 5,那麼就表示現在訊號量可以被進程或者線程使用5次,再之後就要進行等待;如果InitialCount = 2 && MaximumCount == 5這樣的用法不太常見,表示還可以調用兩次CreateSemaphore或者OpenSemaphore,再調用的話就要進入等待狀態。最後一個參數表示這個訊號量的名字,這樣就可以跨進程的時候通過這個名字OpenSemaphore。說了這麼多了,不知道說明白沒有~
看個例子,popo現在好像在本機只能運行三個執行個體,我們在前面說的mutex可以讓程式只是運行一個執行個體,下面我通過訊號量機制讓程式像popo一樣運行三個執行個體。
#include "stdafx.h"
#include <windows.h>
#include <iostream>
using namespace std;
const int MAX_RUNNUM = 3; //最多運行執行個體個數
void PrintInfo()
{
char c;
cout << "run program" << endl;
cout << "input s to exit program!" << endl;
while (1)
{
cin >> c;
if (c == 's')
{
break;
}
Sleep(10);
}
}
int main(int argc, char* argv[])
{
HANDLE hSe = CreateSemaphore(NULL, MAX_RUNNUM, MAX_RUNNUM, "semaphore_test");
DWORD ret = 0;
if (hSe == NULL)
{
cout << "createsemaphore failed with code: " << GetLastError() << endl;
return -1;
}
ret = WaitForSingleObject(hSe, 1000);
if (ret == WAIT_TIMEOUT)
{
cout << "you have runned " << MAX_RUNNUM << " program!" << endl;
ret = WaitForSingleObject(hSe, INFINITE);
}
PrintInfo();
ReleaseSemaphore(hSe, 1, NULL);
CloseHandle(hSe);
return 0;
}
附錄:
核心對象
Change notification
Console input
Event
Job
Mutex
Process
Semaphore
Thread
Waitable timer