您的位置:首页 > 其它

关键段(Critical Section)

2015-09-14 14:49 302 查看

关键段

关键段是一小段代码,它在执行之前需要独占对一些共享资源的的访问权。该方式以“原子方式”对资源进行操控。在当前线程离开关键段之前,系统是不会调度任何想要访问同一资源的其他线程的。

*注:原子方式,指的是代码知道除了当前线程之外,没有其他任何线程会同时访问该资源。

关键段CRITICAL_SECTION一共就四个函数:

void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

typedef struct _RTL_CRITICAL_SECTION {

PRTL_CRITICAL_SECTION_DEBUG DebugInfo;  // 调试用的

LONG LockCount;  // 初始化为-1,n表示有n个线程在等待

LONG RecursionCount;  // 表示该关键段的拥有线程对此资源获得关键段次数,初为0

HANDLEOwningThread; // 拥有该关键段的线程句柄,from the thread's ClientId->UniqueThread

HANDLE LockSemaphore;  // 实际上是一个自复位事件

DWORD SpinCount;  // 旋转锁的设置,单CPU下忽略

} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;


线程所有权

关键段是有“线程所有权”概念的。事实上它会用第四个参数OwningThread来记录获准进入关键区域的线程句柄,如果这个线程再次进入,EnterCriticalSection()会更新第三个参数RecursionCount以记录该线程进入的次数并立即返回让该线程进入。其它线程调用EnterCriticalSection()则会被切换到等待状态,一旦拥有线程所有权的线程调用LeaveCriticalSection()使其进入的次数为0时,系统会自动更新关键段并将等待中的线程换回可调度状态。

因此可以将关键段比作旅馆的房卡,调用EnterCriticalSection()即申请房卡,得到房卡后自己当然是可以多次进出房间的,在你调用LeaveCriticalSection()交出房卡之前,别人自然是无法进入该房间。

什么时候使用关键段

当我们有一个资源要让多线程访问的时候,应该创建一个CRITICAL_SECTION结构。

1、如果有多个总是应该在一起使用的资源,只需创建一个CRITICAL_SECTION结构来保护所有这些结构。

2、如果有多个不总是一起使用的资源–比如,线程1和线程2访问资源A,而线程1和线程3访问资源B,那么应该为每个资源分别创建一个CRITICAL_SECTION结构。

任何要访问共享资源的代码,都必须包含在EnterCriticalSection和LeaveCriticalSection之间。如果忘了哪怕是一个地方,共享资源就有可能被破坏。

关键段怎么用

一般情况下,我们会将CRITICAL_SECTION结构作为全局变量来分配;

也可以作为局部变量来分配,或从堆中动态分配;

另外将它们作为类的私有字段来分配也很常见。

class CLockSection
{
public:
CLockSection()
{
::InitializeCriticalSection(&_pSec);
}
~CLockSection()
{
::DeleteCriticalSection(&_pSec);
}
void Lock()
{
::EnterCriticalSection(&_pSec);
}
void UnLock()
{
::LeaveCriticalSection(&_pSec);
}
private:
CRITICAL_SECTION _pSec;
};

// 另一种
class CRefCount
{
public:
CRefCount::CRefCount(void)
{
::InitializeCriticalSection(&m_refSec);
}

CRefCount::~CRefCount(void)
{
::DeleteCriticalSection(&m_refSec);
}

inline CRITICAL_SECTION* GetLock()
{
return &m_refSec;
}
private:
CRITICAL_SECTION m_refSec;
};

class CGuard
{
public:
CGuard(CRITICAL_SECTION* pSec)
{
m_pSec = pSec;
if(m_pSec != NULL)
{
::EnterCriticalSection(m_pSec);
}
}

~CGuard()
{
if(m_pSec != NULL)
{
::LeaveCriticalSection(m_pSec);
m_pSec = NULL;
}
}
private:
CRITICAL_SECTION* m_pSec;
};

CRefCount g_ref;  // 全局

{
CGuard guard(g_ref.GetLock());
/*
需要共享访问的操作
*/
}  // 离开作用域,自动析构


关键段的优缺点

优点:容易使用,执行速度快(内部也使用了Interlocked函数)

缺点:无法用来在多个进程之间对线程进行同步;只能用于互斥,不能用于同步

旋转锁

当线程试图进入一个关键段,但这个关键段正被另一个线程占用的时候,函数会立即把调用线程切换到等待状态。这意味着线程必须从用户模式切换到内核模式(大概1000个CPU周期),这个切换的开销非常大。

为了提高关键段的性能,Micsoft把旋转锁合并到了关键段中,当调用EnterCriticalSection的时候,它会用一个旋转锁不断地循环,尝试在一段时间内获得对资源的访问权。只有当尝试失败的时候,线程才会切换到内核模式并进入等待状态。

BOOL InitializeCriticalSectionAndSpinCount(

LPCRITICAL_SECTION lpCriticalSection,  // pointer to critical section

DWORD dwSpinCount)   // spin count for critical section (0 ~ 0x00FFFFFF)


建议:

我们应该总是在使用关键段的时候同时使用旋转锁,因为这样做不会损失任何东西。为了得到最佳的性能,最简单的方法是,尝试各种数值,直到对性能感到满意为止。参考值:4000

参考资料

[1] 《Windows核心编程 第五版》

[2]秒杀多线程第五篇 经典线程同步 关键段CS
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: