作者: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