?? 進程同步.txt
字號:
使用臨界段實現(xiàn)優(yōu)化的進程間同步對象-原理和實現(xiàn)
by Jeffrey.Richter
vcbear 熱情講解
實現(xiàn)自己的同步對象?需要嗎?
不需要嗎?
...
只是跟你研究一下而已.
算了吧我只是個愛灌水的家伙,很久沒有寫代碼了,閑來無事,灌灌水還不行嗎?
1.概述:
在多進程的環(huán)境里,需要對線程進行同步.常用的同步對象有臨界段(Critical Section),互斥量(Mutex),信號量(Semaphore),事件(Event)等,除了臨界段,都是內(nèi)核對象。
在同步技術(shù)中,臨界段(Critical Section)是最容易掌握的,而且,和通過等待和釋放內(nèi)核態(tài)互斥對象實現(xiàn)同步的方式相比,臨界段的速度明顯勝出.但是臨界段有一個缺陷,WIN32文檔已經(jīng)說明了臨界段是不能跨進程的,就是說臨界段不能用在多進程間的線程同步,只能用于單個進程內(nèi)部的線程同步.
因為臨界段只是一個很簡單的數(shù)據(jù)結(jié)構(gòu)體,在別的進程的進程空間里是無效的。就算是把它放到一個可以多進程共享的內(nèi)存映象文件里,也還是無法工作.
有甚么方法可以跨進程的實現(xiàn)線程的高速同步嗎?
2.原理和實現(xiàn)
2.1為什么臨界段快? 是“真的”快嗎?
確實,臨界段要比其他的核心態(tài)同步對象要快,因為EnterCriticalSection和LeaveCriticalSection這兩個函數(shù)從InterLockedXXX系列函數(shù)中得到不少好處(下面的代碼演示了臨界段是如何使用InterLockedXXX函數(shù)的)。InterLockedXXX系列函數(shù)完全運行于用戶態(tài)空間,根本不需要從用戶態(tài)到核心態(tài)
之間的切換。所以,進入和離開一個臨界段一般只需要10個左右的CPU執(zhí)行指令。而當(dāng)調(diào)用WaitForSingleObject之流的函數(shù)時,因為使用了內(nèi)核對象,線程被強制的在用戶態(tài)和核心態(tài)之間變換。在x86處理器上,這種變換一般需要600個CPU指令。看到這里面的巨大差距了把。
話說回來,臨界段是不是真正的“快”?實際上,臨界段只在共享資源沒有沖突的時候是快的。當(dāng)一個線程試圖進入正在被另外一個線程擁有的臨界段,即發(fā)生競爭沖突時,臨界段還是等價于一個event核心態(tài)對象,一樣的需要耗時約600個CPU指令。事實上,因為這樣的競爭情況相對一般的運行情況來說是很少的(除非人為),所以在大部分的時間里(沒有競爭沖突的時候),臨界段的使用根本不牽涉內(nèi)核同步,所以是高速的,只需要10個CPU的指令。(bear說:明白了吧,純屬玩概率,Ms的小花招)
2.3進程邊界怎么辦?
“臨界段等價于一個event核心態(tài)對象”是什么意思?
看看臨界段結(jié)構(gòu)的定義先
typedef struct _RTL_CRITICAL_SECTION {
PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
//
// The following three fields control entering and exiting the critical
// section for the resource
//
LONG LockCount;
LONG RecursionCount;
HANDLE OwningThread; // from the thread''s ClientId->UniqueThread
HANDLE LockSemaphore;
DWORD SpinCount;
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;
#typedef RTL_CRITICAL_SECTION CRITICL_SECTION
在CRITICAL_SECTION 數(shù)據(jù)結(jié)構(gòu)里,有一個Event內(nèi)核對象的句柄(那個undocument的結(jié)構(gòu)體成員LockSemaphore,包含的實際是一個event的句柄, 而不是一個信號量semaphore)。正如我們所知,內(nèi)核對象是系統(tǒng)全局的,但是該句柄是進程所有的,而不是系統(tǒng)全局的。所以,就算把一個臨界段結(jié)構(gòu)直接放到共享的內(nèi)存映象里,臨界段也無法起作用,因為LockSemaphore里句柄值只對一個進程有效,對于別的進程是沒有意義的。 在一般的進程同步中,進程要使用一個存在于別的進程里的Event 對象,必須調(diào)用OpenEvent或CreaetEvent函數(shù)來得到進程可以使用的句柄值。
CRITICAL_SECTION結(jié)構(gòu)里其他的變量是臨界段工作所依賴的元素,Ms也“警告”程序員不要自己改動該結(jié)構(gòu)體里變量的值。是怎么實現(xiàn)的呢?看下一步.
2.4 COptex,優(yōu)化的同步對象類
Jeffrey Richter曾經(jīng)寫過一個自己的臨界段,現(xiàn)在,他把他的臨界段改良了一下,把它封裝成一個COptex類。成員函數(shù)TryEnter擁有NT4里介紹的函數(shù)TryEnterCriticalSection的功能,這個函數(shù)嘗試進入臨界段,如果失敗立刻返回,不會掛起線程,并且支持Spin計數(shù).這個功能在NT4/SP3中被InitializeCriticalSectionAndSpinCount 和SetCriticalSectionSpinCount實現(xiàn)。Spin計數(shù)在多處理器系統(tǒng)和高競爭沖突情況下是很有用的,在進入WaitForXXX核心態(tài)之前,臨界段根據(jù)設(shè)定的Spin計數(shù)進行多次TryEnterCtriticalSection,然后才進行堵塞。想一下,TryEnterCriticalSection才使用10個左右的周期,如果在Spin計數(shù)消耗完之前,沖突消失,臨界段對象是空閑的,那么再用10個CPU周期就可以在用戶態(tài)進入臨界段了,不用切換到核心態(tài).
(bear說:為了避免這個"核心態(tài)",Ms自己也是費勁腦汁呀.看出來了吧,優(yōu)化的原則:在需要的時候才進入核心態(tài)。否則,在用戶態(tài)進行同步)
以下是COptex代碼。原代碼下載
Figure 2: COptex
Optex.h
/******************************************************************************
Module name: Optex.h
Written by: Jeffrey Richter
Purpose: Defines the COptex (optimized mutex) synchronization object
******************************************************************************/
#pragma once
///////////////////////////////////////////////////////////////////////////////
class COptex {
public:
COptex(LPCSTR pszName, DWORD dwSpinCount = 4000);
COptex(LPCWSTR pszName, DWORD dwSpinCount = 4000);
~COptex();
void SetSpinCount(DWORD dwSpinCount);
void Enter();
BOOL TryEnter();
void Leave();
private:
typedef struct {
DWORD m_dwSpinCount;
long m_lLockCount;
DWORD m_dwThreadId;
long m_lRecurseCount;
} SHAREDINFO, *PSHAREDINFO;
BOOL m_fUniprocessorHost;
HANDLE m_hevt;
HANDLE m_hfm;
PSHAREDINFO m_pSharedInfo;
private:
BOOL CommonConstructor(PVOID pszName, BOOL fUnicode, DWORD dwSpinCount);
};
///////////////////////////////////////////////////////////////////////////////
inline COptex::COptex(LPCSTR pszName, DWORD dwSpinCount) {
CommonConstructor((PVOID) pszName, FALSE, dwSpinCount);
}
///////////////////////////////////////////////////////////////////////////////
inline COptex::COptex(LPCWSTR pszName, DWORD dwSpinCount) {
CommonConstructor((PVOID) pszName, TRUE, dwSpinCount);
}
Optex.cpp
/******************************************************************************
Module name: Optex.cpp
Written by: Jeffrey Richter
Purpose: Implements the COptex (optimized mutex) synchronization object
******************************************************************************/
#include <windows.h>
#include "Optex.h"
///////////////////////////////////////////////////////////////////////////////
BOOL COptex::CommonConstructor(PVOID pszName, BOOL fUnicode, DWORD dwSpinCount)
{
m_hevt = m_hfm = NULL;
m_pSharedInfo = NULL;
SYSTEM_INFO sinf;
GetSystemInfo(&sinf);
m_fUniprocessorHost = (sinf.dwNumberOfProcessors == 1);
char szNameA[100];
if (fUnicode) { // Convert Unicode name to ANSI
wsprintfA(szNameA, "%S", pszName);
pszName = (PVOID) szNameA;
}
char sz[100];
wsprintfA(sz, "JMR_Optex_Event_%s", pszName);
m_hevt = CreateEventA(NULL, FALSE, FALSE, sz);
if (m_hevt != NULL) {
wsprintfA(sz, "JMR_Optex_MMF_%s", pszName);
m_hfm = CreateFileMappingA(NULL, NULL, PAGE_READWRITE, 0, sizeof(*m_pSharedInfo), sz);
if (m_hfm != NULL) {
m_pSharedInfo = (PSHAREDINFO) MapViewOfFile(m_hfm, FILE_MAP_WRITE,
0, 0, 0);
// Note: SHAREDINFO''s m_lLockCount, m_dwThreadId, and m_lRecurseCount
// members need to be initialized to 0. Fortunately, a new pagefile
// MMF sets all of its data to 0 when created. This saves us from
// some thread synchronization work.
if (m_pSharedInfo != NULL)
SetSpinCount(dwSpinCount);
}
}
return((m_hevt != NULL) && (m_hfm != NULL) && (m_pSharedInfo != NULL));
}
///////////////////////////////////////////////////////////////////////////////
COptex::~COptex() {
#ifdef _DEBUG
if (m_pSharedInfo->m_dwThreadId != 0) DebugBreak();
#endif
UnmapViewOfFile(m_pSharedInfo);
CloseHandle(m_hfm);
CloseHandle(m_hevt);
}
///////////////////////////////////////////////////////////////////////////////
void COptex::SetSpinCount(DWORD dwSpinCount) {
if (!m_fUniprocessorHost)
InterlockedExchange((PLONG) &m_pSharedInfo->m_dwSpinCount, dwSpinCount);
}
///////////////////////////////////////////////////////////////////////////////
void COptex::Enter() {
// Spin, trying to get the Optex
if (TryEnter()) return;
DWORD dwThreadId = GetCurrentThreadId(); // The calling thread''s ID
if (InterlockedIncrement(&m_pSharedInfo->m_lLockCount) == 1) {
// Optex is unowned, let this thread own it once
InterlockedExchange((PLONG) &m_pSharedInfo->m_dwThreadId, dwThreadId);
m_pSharedInfo->m_lRecurseCount = 1;
} else {
// Optex is owned by a thread
if (m_pSharedInfo->m_dwThreadId == dwThreadId) {
// Optex is owned by this thread, own it again
m_pSharedInfo->m_lRecurseCount++;
} else {
// Optex is owned by another thread
// Wait for the Owning thread to release the Optex
WaitForSingleObject(m_hevt, INFINITE);
// We got ownership of the Optex
InterlockedExchange((PLONG) &m_pSharedInfo->m_dwThreadId,
dwThreadId); // We own it now
m_pSharedInfo->m_lRecurseCount = 1; // We own it once
}
}
}
///////////////////////////////////////////////////////////////////////////////
BOOL COptex::TryEnter() {
DWORD dwThreadId = GetCurrentThreadId(); // The calling thread''s ID
// If the lock count is zero, the Optex is unowned and
// this thread can become the owner of it now.
BOOL fThisThreadOwnsTheOptex = FALSE;
DWORD dwSpinCount = m_pSharedInfo->m_dwSpinCount;
do {
fThisThreadOwnsTheOptex = (0 == (DWORD)
InterlockedCompareExchange((PVOID*) &m_pSharedInfo->m_lLockCount,
(PVOID) 1, (PVOID) 0));
if (fThisThreadOwnsTheOptex) {
// We now own the Optex
InterlockedExchange((PLONG) &m_pSharedInfo->m_dwThreadId,
dwThreadId); // We own it
m_pSharedInfo->m_lRecurseCount = 1; // We own it once
} else {
// Some thread owns the Optex
if (m_pSharedInfo->m_dwThreadId == dwThreadId) {
// We already own the Optex
InterlockedIncrement(&m_pSharedInfo->m_lLockCount);
m_pSharedInfo->m_lRecurseCount++; // We own it again
fThisThreadOwnsTheOptex = TRUE; // Return that we own the Optex
}
}
} while (!fThisThreadOwnsTheOptex && (dwSpinCount-- > 0));
// Return whether or not this thread owns the Optex
return(fThisThreadOwnsTheOptex);
}
///////////////////////////////////////////////////////////////////////////////
void COptex::Leave() {
#ifdef _DEBUG
if (m_pSharedInfo->m_dwThreadId != GetCurrentThreadId())
DebugBreak();
#endif
if (--m_pSharedInfo->m_lRecurseCount > 0) {
// We still own the Optex
InterlockedDecrement(&m_pSharedInfo->m_lLockCount);
} else {
// We don''t own the Optex
InterlockedExchange((PLONG) &m_pSharedInfo->m_dwThreadId, 0);
if (InterlockedDecrement(&m_pSharedInfo->m_lLockCount) > 0) {
// Other threads are waiting, wake one of them
SetEvent(m_hevt);
}
}
}
///////////////////////////////// End of File /////////////////////////////////
使用這個COptex是很簡單的事情,只要構(gòu)造用下面這兩種構(gòu)造函數(shù)一個C++類的實例即可.
構(gòu)造函數(shù)
COptex(LPCSTR pszName, DWORD dwSpinCount = 4000);
COptex(LPCWSTR pszName, DWORD dwSpinCount = 4000);
他們都調(diào)用了
BOOL CommonConstructor(PVOID pszName, BOOL fUnicode, DWORD dwSpinCount);
構(gòu)造一個COptex對象必須給它一個字符串型的名字,在突破進程邊界的時候這是必須的,只有這個名字能提供共享訪問.構(gòu)造函數(shù)支持ANSI或Unicode的名字。
當(dāng)另外一個進程使用相同的名字構(gòu)造一個COptex對象,構(gòu)造函數(shù)如何發(fā)現(xiàn)已經(jīng)存在的COptex對象?在CommonConstructor代碼中用CreateEvent嘗試創(chuàng)建一個命名Event對象,如果這個名字的Event對象已經(jīng)存在,那么,得到該對象的句柄,并且GetLastError可以得到ERROR_ALREADY_EXISTS.如果不存在則創(chuàng)建一個.如果創(chuàng)建失敗,則得到的句柄為NULL.
同樣的,可以得到一個共享的內(nèi)存映象文件的句柄.
構(gòu)造成功后,在需要同步時,根據(jù)情況簡單的執(zhí)行相應(yīng)的進程間同步操作。構(gòu)造函數(shù)的第二個參數(shù)用來指定Spin計數(shù),默認是4000(這是操作系統(tǒng)序列化堆Heap的函數(shù)所使用的數(shù)量.操作系統(tǒng)在分配和釋放內(nèi)存的時候,要序列化進程的堆,這時也要用到臨界段)
COptex類的其他函數(shù)和Win32函數(shù)是一一對應(yīng)的.熟悉同步對象的程序員應(yīng)該很容易理解.
COptex是如何工作的呢?實際上,一個COptex包含兩個數(shù)據(jù)塊(Data blocks):一個本地的,私有的;另一個是全局的,共享的.一個COptex對象構(gòu)造之后,本地數(shù)據(jù)塊包含了COptex的成員變量:m_hevt變量初始化為一個命名事件對象句柄;m_hfm變量初始化為一個內(nèi)存映象文件對象句柄.既然這些句柄代表的對象是命名的,那么,他們可以在進程間共享。注意,是"對象"可以共享,而不是"對象的句柄".每個進程內(nèi)的COptex對象都必須保持這些句柄在本進程內(nèi)的值.
m_pShareInf成員指向一個內(nèi)存映象文件,全局數(shù)據(jù)塊在這個內(nèi)存映象文件里,以指定的共享名存在. SHAREDINFO結(jié)構(gòu)是內(nèi)存映象數(shù)據(jù)的組織方式,該結(jié)構(gòu)在COptex類里定義,和CRITCIAL_SECTION的結(jié)構(gòu)非常相似.
typedef struct {
DWORD m_dwSpinCount;
long m_lLockCount;
DWORD m_dwThreadId;
long m_lRecurseCount;report-2001-03-07.htm
} SHAREDINFO, *PSHAREDINFO;
m_dwSpinCount : spin計數(shù)
m_lLockCount : 鎖定計數(shù)
m_dwThreadID : 擁有該臨界段的線程ID
m_lRecurseCount:本線程擁有該臨界段的計數(shù)
好了,仔細看看代碼吧,大師風(fēng)范呀.注意一下在進行同步時,關(guān)于是否同一線程,關(guān)于LockCount的值的一系列的判斷,以及InterLockedXXX系列函數(shù)的使用,具體用法查MSDN.
bear最喜歡這樣的代碼了,簡單明了,思路清晰,原理超值,看完了只想大喝一聲"又學(xué)一招,爽!"
bear也寫累了 ,收工:).
2001.3.2
隨意轉(zhuǎn)載,只要不去掉Jeffrey的名字,還有bear的:D
翻譯有錯,請找vcbear@sina.com或留言,不懂Win32編程看下面:
Have a question about programming in Win32? Contact Jeffrey Richter at http://www.jeffreyrichter.com/
From the January 1998 issue of Microsoft Systems Journal.
<!--article end-->
?? 快捷鍵說明
復(fù)制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -