使用临界段实现优化的进程间同步对象-原理和实现
1.概述:
在多進(jìn)程的環(huán)境里,需要對(duì)線程進(jìn)行同步.常用的同步對(duì)象有臨界段(Critical Section),互斥量(Mutex),信號(hào)量(Semaphore),事件(Event)等,除了臨界段,都是內(nèi)核對(duì)象。
在同步技術(shù)中,臨界段(Critical Section)是最容易掌握的,而且,和通過等待和釋放內(nèi)核態(tài)互斥對(duì)象實(shí)現(xiàn)同步的方式相比,臨界段的速度明顯勝出.但是臨界段有一個(gè)缺陷,WIN32文檔已經(jīng)說明了臨界段是不能跨進(jìn)程的,就是說臨界段不能用在多進(jìn)程間的線程同步,只能用于單個(gè)進(jìn)程內(nèi)部的線程同步.
因?yàn)榕R界段只是一個(gè)很簡(jiǎn)單的數(shù)據(jù)結(jié)構(gòu)體,在別的進(jìn)程的進(jìn)程空間里是無效的。就算是把它放到一個(gè)可以多進(jìn)程共享的內(nèi)存映象文件里,也還是無法工作.
有甚么方法可以跨進(jìn)程的實(shí)現(xiàn)線程的高速同步嗎?
2.原理和實(shí)現(xiàn)
2.1為什么臨界段快? 是“真的”快嗎?
確實(shí),臨界段要比其他的核心態(tài)同步對(duì)象要快,因?yàn)镋nterCriticalSection和LeaveCriticalSection這兩個(gè)函數(shù)從InterLockedXXX系列函數(shù)中得到不少好處(下面的代碼演示了臨界段是如何使用InterLockedXXX函數(shù)的)。InterLockedXXX系列函數(shù)完全運(yùn)行于用戶態(tài)空間,根本不需要從用戶態(tài)到核心態(tài)
之間的切換。所以,進(jìn)入和離開一個(gè)臨界段一般只需要10個(gè)左右的CPU執(zhí)行指令。而當(dāng)調(diào)用WaitForSingleObject之流的函數(shù)時(shí),因?yàn)槭褂昧藘?nèi)核對(duì)象,線程被強(qiáng)制的在用戶態(tài)和核心態(tài)之間變換。在x86處理器上,這種變換一般需要600個(gè)CPU指令。看到這里面的巨大差距了把。
話說回來,臨界段是不是真正的“快”?實(shí)際上,臨界段只在共享資源沒有沖突的時(shí)候是快的。當(dāng)一個(gè)線程試圖進(jìn)入正在被另外一個(gè)線程擁有的臨界段,即發(fā)生競(jìng)爭(zhēng)沖突時(shí),臨界段還是等價(jià)于一個(gè)event核心態(tài)對(duì)象,一樣的需要耗時(shí)約600個(gè)CPU指令。事實(shí)上,因?yàn)檫@樣的競(jìng)爭(zhēng)情況相對(duì)一般的運(yùn)行情況來說是很少的(除非人為),所以在大部分的時(shí)間里(沒有競(jìng)爭(zhēng)沖突的時(shí)候),臨界段的使用根本不牽涉內(nèi)核同步,所以是高速的,只需要10個(gè)CPU的指令。(bear說:明白了吧,純屬玩概率,Ms的小花招)
2.3進(jìn)程邊界怎么辦?
“臨界段等價(jià)于一個(gè)event核心態(tài)對(duì)象”是什么意思?
看看臨界段結(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)里,有一個(gè)Event內(nèi)核對(duì)象的句柄(那個(gè)undocument的結(jié)構(gòu)體成員LockSemaphore,包含的實(shí)際是一個(gè)event的句柄, 而不是一個(gè)信號(hào)量semaphore)。正如我們所知,內(nèi)核對(duì)象是系統(tǒng)全局的,但是該句柄是進(jìn)程所有的,而不是系統(tǒng)全局的。所以,就算把一個(gè)臨界段結(jié)構(gòu)直接放到共享的內(nèi)存映象里,臨界段也無法起作用,因?yàn)長ockSemaphore里句柄值只對(duì)一個(gè)進(jìn)程有效,對(duì)于別的進(jìn)程是沒有意義的。 在一般的進(jìn)程同步中,進(jìn)程要使用一個(gè)存在于別的進(jìn)程里的Event 對(duì)象,必須調(diào)用OpenEvent或CreaetEvent函數(shù)來得到進(jìn)程可以使用的句柄值。
CRITICAL_SECTION結(jié)構(gòu)里其他的變量是臨界段工作所依賴的元素,Ms也“警告”程序員不要自己改動(dòng)該結(jié)構(gòu)體里變量的值。是怎么實(shí)現(xiàn)的呢?看下一步.
2.4 COptex,優(yōu)化的同步對(duì)象類
Jeffrey Richter曾經(jīng)寫過一個(gè)自己的臨界段,現(xiàn)在,他把他的臨界段改良了一下,把它封裝成一個(gè)COptex類。成員函數(shù)TryEnter擁有NT4里介紹的函數(shù)TryEnterCriticalSection的功能,這個(gè)函數(shù)嘗試進(jìn)入臨界段,如果失敗立刻返回,不會(huì)掛起線程,并且支持Spin計(jì)數(shù).這個(gè)功能在NT4/SP3中被InitializeCriticalSectionAndSpinCount 和SetCriticalSectionSpinCount實(shí)現(xiàn)。Spin計(jì)數(shù)在多處理器系統(tǒng)和高競(jìng)爭(zhēng)沖突情況下是很有用的,在進(jìn)入WaitForXXX核心態(tài)之前,臨界段根據(jù)設(shè)定的Spin計(jì)數(shù)進(jìn)行多次TryEnterCtriticalSection,然后才進(jìn)行堵塞。想一下,TryEnterCriticalSection才使用10個(gè)左右的周期,如果在Spin計(jì)數(shù)消耗完之前,沖突消失,臨界段對(duì)象是空閑的,那么再用10個(gè)CPU周期就可以在用戶態(tài)進(jìn)入臨界段了,不用切換到核心態(tài).
(bear說:為了避免這個(gè)"核心態(tài)",Ms自己也是費(fèi)勁腦汁呀.看出來了吧,優(yōu)化的原則:在需要的時(shí)候才進(jìn)入核心態(tài)。否則,在用戶態(tài)進(jìn)行同步)
以下是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 /
使用這個(gè)COptex是很簡(jiǎn)單的事情,只要構(gòu)造用下面這兩種構(gòu)造函數(shù)一個(gè)C++類的實(shí)例即可.
構(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)造一個(gè)COptex對(duì)象必須給它一個(gè)字符串型的名字,在突破進(jìn)程邊界的時(shí)候這是必須的,只有這個(gè)名字能提供共享訪問.構(gòu)造函數(shù)支持ANSI或Unicode的名字。
當(dāng)另外一個(gè)進(jìn)程使用相同的名字構(gòu)造一個(gè)COptex對(duì)象,構(gòu)造函數(shù)如何發(fā)現(xiàn)已經(jīng)存在的COptex對(duì)象?在CommonConstructor代碼中用CreateEvent嘗試創(chuàng)建一個(gè)命名Event對(duì)象,如果這個(gè)名字的Event對(duì)象已經(jīng)存在,那么,得到該對(duì)象的句柄,并且GetLastError可以得到ERROR_ALREADY_EXISTS.如果不存在則創(chuàng)建一個(gè).如果創(chuàng)建失敗,則得到的句柄為NULL.
同樣的,可以得到一個(gè)共享的內(nèi)存映象文件的句柄.
構(gòu)造成功后,在需要同步時(shí),根據(jù)情況簡(jiǎn)單的執(zhí)行相應(yīng)的進(jìn)程間同步操作。構(gòu)造函數(shù)的第二個(gè)參數(shù)用來指定Spin計(jì)數(shù),默認(rèn)是4000(這是操作系統(tǒng)序列化堆Heap的函數(shù)所使用的數(shù)量.操作系統(tǒng)在分配和釋放內(nèi)存的時(shí)候,要序列化進(jìn)程的堆,這時(shí)也要用到臨界段)
COptex類的其他函數(shù)和Win32函數(shù)是一一對(duì)應(yīng)的.熟悉同步對(duì)象的程序員應(yīng)該很容易理解.
COptex是如何工作的呢?實(shí)際上,一個(gè)COptex包含兩個(gè)數(shù)據(jù)塊(Data blocks):一個(gè)本地的,私有的;另一個(gè)是全局的,共享的.一個(gè)COptex對(duì)象構(gòu)造之后,本地?cái)?shù)據(jù)塊包含了COptex的成員變量:m_hevt變量初始化為一個(gè)命名事件對(duì)象句柄;m_hfm變量初始化為一個(gè)內(nèi)存映象文件對(duì)象句柄.既然這些句柄代表的對(duì)象是命名的,那么,他們可以在進(jìn)程間共享。注意,是"對(duì)象"可以共享,而不是"對(duì)象的句柄".每個(gè)進(jìn)程內(nèi)的COptex對(duì)象都必須保持這些句柄在本進(jìn)程內(nèi)的值.
m_pShareInf成員指向一個(gè)內(nèi)存映象文件,全局?jǐn)?shù)據(jù)塊在這個(gè)內(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計(jì)數(shù)
m_lLockCount : 鎖定計(jì)數(shù)
m_dwThreadID : 擁有該臨界段的線程ID
m_lRecurseCount:本線程擁有該臨界段的計(jì)數(shù)
好了,仔細(xì)看看代碼吧,大師風(fēng)范呀.注意一下在進(jìn)行同步時(shí),關(guān)于是否同一線程,關(guān)于LockCount的值的一系列的判斷,以及InterLockedXXX系列函數(shù)的使用,具體用法查MSDN.
bear最喜歡這樣的代碼了,簡(jiǎn)單明了,思路清晰,原理超值,看完了只想大喝一聲"又學(xué)一招,爽!"
bear也寫累了 ,收工:).
2001.3.2隨意轉(zhuǎn)載,只要不去掉Jeffrey的名字,還有bear的:D
翻譯有錯(cuò),請(qǐng)找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.
總結(jié)
以上是生活随笔為你收集整理的使用临界段实现优化的进程间同步对象-原理和实现的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Unet项目解析(4): ./src/
- 下一篇: Unet项目解析(5): 数据封装、数