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