使用临界段实现优化的进程间同步对象-原理和实现

原创 2001年09月28日 10:12:00

使用临界段实现优化的进程间同步对象-原理和实现

by Jeffrey.Richter

vcbear 热情讲解

实现自己的同步对象?需要吗?

不需要吗?

...

只是跟你研究一下而已.

算了吧我只是个爱灌水的家伙,很久没有写代码了,闲来无事,灌灌水还不行吗?

 

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.

<!--article end-->

秒杀多线程第五篇---经典线程同步 关键段(临界区)CS

上一篇《秒杀多线程第四篇 一个经典的多线程同步问题》提出了一个经典的多线程同步互斥问题,本篇将用关键段CRITICAL_SECTION来尝试解决这个问题。本文首先介绍下如何使用关键段,然后再深层次的分...
  • will130
  • will130
  • 2015年11月12日 21:24
  • 500

MFC线程(二):线程同步临界区CRITICAL SECTION

当多个线程同时使用相同的资源时,由于是并发执行,不能保证先后顺序.所以假如时一个公共变量被几个线程同时使用会造成该变量值的混乱. 下面来举个简单例子. 假如有一个字符数组变量 char g_ch...
  • weiwenhp
  • weiwenhp
  • 2013年03月08日 15:58
  • 7801

进程间通信 和 线程间同步

以前经常搞混,所以记录下来。 进程间通信主要是指多个进程间的数据交互。 而进程/线程间同步主要指维护多个进程/线程之间数据准确、一致性。 一.进程间通信主要有以下几种方式: 管道(pipe):...
  • majianfei1023
  • majianfei1023
  • 2016年05月31日 23:47
  • 1471

进程同步的基本概念:临界资源、同步和互斥

在多道程序环境下,进程是并发执行的,不同进程之间存在着不同的相互制约关系。为了协调进程之间的相互制约关系,引入了进程同步的概念。 临界资源 虽然多个进程可以共享系统中的各种资源,但其中许多资源...
  • qq_23930393
  • qq_23930393
  • 2016年09月05日 13:33
  • 392

临界区(临界段)的含义

临界区  不论是硬件临界资源,还是软件临界资源,多个进程必须互斥地对它进行访问。每个进程中访问临界资源的那段代码称为临界区(Critical Section)。    每个进程中访问临界资源的那段程...
  • dgergeg
  • dgergeg
  • 2015年11月23日 21:10
  • 2065

操作系统 4. 同步问题;

这个文章会首先会介绍下竞争条件的定义并举例说明下竞争条件可能造成的问题,之后会说一下临界区的概念和临界区问题的解决。 最后会讨论下信号量。...
  • Sun_Rider
  • Sun_Rider
  • 2017年12月12日 01:10
  • 36

linux并发同步之基础概念(竞态,并发,临界区)

竞态(race condition)     软件层面上,竞态是指多个线程或进程读写一个共享资源(或共享设备)时的输出结果依赖于线程或进程的先后执行顺序或者时间;(更权威的介绍可以看wiki--htt...
  • Xiang_Cheung
  • Xiang_Cheung
  • 2015年09月08日 21:05
  • 634

windows核心编程-关键段(临界区)线程同步

windows核心编程-关键段(临界区)线程同步 线程同步的方式主要有:临界区、互斥区、事件、信号量四种方式。 接下来我主要讲一下自己在学习windows核心编程中对于临界区线程同步方式的使用。 临界...
  • windows_nt
  • windows_nt
  • 2013年05月20日 23:54
  • 6271

初学者浅谈“进程间通信的同步和互斥的比较简单的作用和用法”

第一次发blog,若有错误请谅解和指导,谢谢!!!! 好了,我们回归正题: 随着时代的发展,线程应运而生。这是为什么呢?这是因为我们要进一步减少CPU的空转时间,支持多处理器以及减少上下文切换的开销,...
  • hbz1993
  • hbz1993
  • 2014年11月26日 19:26
  • 726

【Java】利用synchronized(this)完成线程的临界区

在《【Java】线程并发、互斥与同步》(点击打开链接)中利用了操作系统通过操作信号量控制的原始方法,完成了线程的互斥与同步,说句题外话,其实这个信号量的算法,是著名的迪杰斯特拉创造的,也就是数据结构、...
  • yongh701
  • yongh701
  • 2015年01月20日 20:15
  • 1588
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:使用临界段实现优化的进程间同步对象-原理和实现
举报原因:
原因补充:

(最多只允许输入30个字)