8.4 关键段(临界区)——内部也是使用Interlocked函数来实现的!

8.4.1 关键段的细节

(1)CRITICAL_SECTION的使用方法

  ①CRITICAL_SECTION cs;            //声明为全局变量(也可是成员变量,甚至局部变量)

  ②InitializeCriticalSection(&cs);     //初始化临界区,注意cs是临界区对象,不能被移动和复制

  ③EnterCriticalSection(&cs);        //进入或等待临界区(任何要访问共享资源的代码都须包含应在

//Enter和Leave之间,如果忘了哪怕一个地方,都可能破坏资源)

  ④LeaveCriticalSection(&cs);       //离开临界区

  ⑤DeleteCriticalSection(&cs);      //不再需要临界区对象。

(2)CRITICAL_SECTION数据结构

  ①LockCount字段:最重要的字段,初始化为-1。该字段在XP和Vista以后版本含义有所不同,在Vista以后版本中。

    A、最低位——0表示临界区被锁,1表示没被锁。-->0x1 & LockCount

    B、第2位(低位数起):1表示没有线程被唤醒。0表示一个线程被唤醒 -->(0x2 & lockCount)>>1;

     C、其余各位表示等待锁的线程数量(-1-lockCount)>>2

  ②RecursionCount:表示拥有者线程己经获得该临界区的次数,初始值为0。当拥有者线程每调用EnterCriticalSection时会递增1。也就是说只有拥有者调用EnterCriticalSection时RecursionCount才递增。但为了防止拥有者线程一直霸占临界区,系统允许其他线程调用LeaveCriticalSection使该值递减。但不管是拥有者线程还是其他线程,调用Leave的总次数应该等于Enter的次数,才能被解开锁,当RecursionCount<=0时,锁就解开,OwningThread被设为0。

  ③OwningThread:此字段表示当前占用该临界区的线程ID

  ④LockSemaphore:此字段命名不恰当,它实际上是一个事件对象,用于通知操作系统该临界区已被释放,等待该临界区的线程之一现在可以获得该临界区并继续执行。因为系统是在临界区阻止另一个线程时才自动分配事件句柄,所以如果在不再需要临界区时要将其删除,以防止LockSemaphore 字段可能会导致内存泄漏。

(3)EnterCriticalSection函数的执行过程

  ①如果没有线程正在访问资源,那么EnterCritical会更新临界区成员变量,以表示调用线程己经获得临界区锁,并立即返回,如此调用线程继续执行。

  ②当调用线程获得临界区锁后,如果此时调用线程再次Enter,则会更新RecursionCount,以表示调用线程被获准访问的次数。并立即返回。

  ③当调用线程获得临界区锁后,如果此时是其他线程要进入临界区,则EnterCriticalSection会使用一个事件内核对象(lockSemaphore)将这个线程切换到等待状态。(注意,这些等待的线程会事件内核对象记录下来,以表示正在等待该内核对象的都有哪些线程,当然线程本身也会记录,他在等哪些内核对象)(注意:临界区本身不是内核对象!

(4)LeaveCriticalSection函数:会更新CRITICAL_SECTION的成员变量,如果此时仍有线程处于等待状态,那么该函数会将其中之一的等待线程换回可调度状态

(5)TryEnterCriticalSection函数

  ①该函数不会让调用线程进入等待状态,它通过返回值来表示是否获准访问资源。TRUE表示获准,FALSE表示其他线程正在使用资源,申请被拒绝。

  ②如果返回TRUE时,说明该调用线程己经正在访问资源,CRITICAL_SECTION 成员变量被更新过。所以每个返回TRUE的TryEnterCriticalSection都须调用LeaveCriticalSection

【CriticalsectionInfo程序】

#include <windows.h>
#include <malloc.h>
#include <tchar.h>
#include <locale.h> #define THREADNUM 3 CRITICAL_SECTION g_cs;
HANDLE* hThread = NULL;
/*
临界区中LockCount和RecursionCount字段的含义
1、XP和Windows2000下
(1)LockCount:
①初始为-1,每调用EnterCriticalSection时LockCount加1,调用LeaveCriticalSection减1
②如LockCount = 5 表示某一线程正在使用临界区,此外还有5个线程正在等待锁
(2)RecursionCount:调用线程多次调用EnterCriticalSection的次数
(3)EntryCount:除了调用线程以外的其他线程调用EnterCriticalSection的次数。
(4)当第1次调用EnterCriticalSection后,LockCount、RecursionCount、EntryCount、ContentionCount各加1,
OwningThread设为调用线程的ID。
A、当拥有者再次调用EnterCriticalSection:LockCount++,Recursion++、EntryCount不变
B、当其他线程调用EnterCriticalSection:LockCount++、EntryCount++、Recursion不变
C、当拥有者调用LeaveCriticalSection:LockCount减1(到-1)、Recursion减到0,OwningThread设为0.
D、其他线程调用LeaveCriticalSection,与拥有者调用LeaveCriticalSection变化一样。
2、Windows2003sp1及以后
(1)LockCount:
A、最低位——0表示临界区被锁,1表示没被锁。-->0x1 & LockCount
B、第2位(低位数起):1表示没有线程被唤醒。0表示一个线程被唤醒 -->(0x2 & lockCount)>>1;
C、其余各位表示等待锁的线程数量(-1-lockCount)>>2
*/
//////////////////////////////////////////////////////////////////////////
void ShowCriticalSectionInfo(PCRITICAL_SECTION pcs)
{
_tprintf(_T("Thread[%d],CriticalSection Information:\n"),GetCurrentThreadId());
_tprintf(_T("---------------------------\n"));
_tprintf(_T("LockCount:%d(临界区被锁:%s;是否有线程唤醒:%s;等待锁的线程数量:%d)\n"),
pcs->LockCount,
(0x1 & pcs->LockCount) ? _T("否") : _T("是"),
((0x2 & pcs->LockCount)) >> ? _T("否") : _T("是"),
((- - pcs->LockCount) >> )); _tprintf(_T("RecursionCount:%d\n"), pcs->RecursionCount);
_tprintf(_T("OwningThread:%d\n"), pcs->OwningThread);
_tprintf(_T("LockSemephore:0x%08X\n"), pcs->LockSemaphore);
//_tprintf(_T("SpinCount:0x%08X\n"), pcs->SpinCount);
} //////////////////////////////////////////////////////////////////////////
//线程函数1
DWORD WINAPI ThreadProc1(PVOID pParam)
{
//1、演示First进程两次进入临界区。
_tprintf(_T("第1个子线程[%d]两次进入临界区\n"),GetCurrentThreadId());
EnterCriticalSection(&g_cs);
EnterCriticalSection(&g_cs);
ShowCriticalSectionInfo(&g_cs); return ;
} //线程函数2
DWORD WINAPI ThreadProc2(PVOID pParam)
{
int nIndex = (int)pParam;
//第nIndex个子程线进入临界区,并拿到锁
_tprintf(_T("\n第%d个子线程[%d]尝试进入临界区\n"), nIndex,GetCurrentThreadId());
EnterCriticalSection(&g_cs);
ShowCriticalSectionInfo(&g_cs); _tprintf(_T("\n第%d个子线程己经进入临界区......\n"), nIndex, GetCurrentThreadId()); _tprintf(_T("\n第%d个子线程线程[%d]离开临界区\n"), nIndex, GetCurrentThreadId());
LeaveCriticalSection(&g_cs);
ShowCriticalSectionInfo(&g_cs);
return ;
} int _tmain()
{
_tsetlocale(LC_ALL, _T("chs")); InitializeCriticalSection(&g_cs);//初始化临界区时的状态: hThread = (HANDLE*)malloc(sizeof(HANDLE)*THREADNUM);
hThread[] = CreateThread(NULL, , ThreadProc1, NULL, CREATE_SUSPENDED, NULL);
hThread[] = CreateThread(NULL, , ThreadProc2, (LPVOID), CREATE_SUSPENDED, NULL);
hThread[] = CreateThread(NULL, , ThreadProc2, (LPVOID), CREATE_SUSPENDED, NULL);
ResumeThread(hThread[]);
WaitForSingleObject(hThread[], INFINITE);
_tsystem(_T("PAUSE")); //演示在子线程拥有临界区,但在其他线程(主线程)释放(因子线程两次Enter,主线程要两次Leave)
_tprintf(_T("\n主线程[%d]解开临界区锁\n"), GetCurrentThreadId());
LeaveCriticalSection(&g_cs);
LeaveCriticalSection(&g_cs);
ShowCriticalSectionInfo(&g_cs);
_tsystem(_T("PAUSE")); //第2个子线程启动,进入尝试进入临界区(应该可行,因为锁被主线程释放)
ResumeThread(hThread[]);
WaitForSingleObject(hThread[], INFINITE);
_tsystem(_T("PAUSE")); //主线程锁一下临界区,并启动第3个线程
_tprintf(_T("\n主线程[%d]锁定临界区\n"), GetCurrentThreadId());
EnterCriticalSection(&g_cs);
ResumeThread(hThread[]); //此时第3个线程进入等待状态
Sleep();
ShowCriticalSectionInfo(&g_cs); //主线程解开临界区锁,并恢复第3个线程
_tprintf(_T("\n主线程[%d]解开临界区锁\n"), GetCurrentThreadId());
LeaveCriticalSection(&g_cs);
Sleep(); _tsystem(_T("PAUSE")); WaitForMultipleObjects(THREADNUM, hThread, TRUE, INFINITE);
for (int i = ; i < THREADNUM;i++){
CloseHandle(hThread[i]);
}
free(hThread);
DeleteCriticalSection(&g_cs); return ;
}

8.4.2 关键段和旋转锁

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

(2)但往往当前占用资源的线程可能很快就结束对资源的访问,事实上,在需要等待的线程完成切换到内核模式之前,占用资源的线程可以己经释放了资源,这无法浪费大量CPU时间。

(3)为了提高关键段的性能,可加入合并旋转锁到关键段中。当调用EnterCriticalSection时,先尝试旋转方式的访问资源。只有尝试一个后仍失败。才切换到内核模式并进入等待状态。

(4)要使用具有旋转方式的关键段,必须调用以下函数来初始化关键段 InitializeCriticalSectionAndSpinCount(pcs,dwSpinCount);其中 dwSpinCount为旋转次数(如4000)。如果在单CPU机器上,系统会忽图dwSpinCount参数。

(5)改变关键段的旋转次数SetCriticalSectionSpinCount(pcs,dwSpinCount);

8.4.3 关键段和错误处理

(1)InitializeCriticalSection返回值为VOID,这是Microsoft设计时考虑不周。实际上该函数调用仍可能失败,如给关键分配内存时,当失败时将抛出STATUS_NO_MEMORY异常。

(2)InitializeCriticalSectionAndSpinCount失败时将返回FALSE

(3)关键段内部使用的事件内核对象只有在两个(或多个)线程在同一时刻争夺同一关键段时才会创建它。这样做是为了节省系统资源。只有在DeleteCriticalSection后,该内核对象才会被释放(因此,用完关键段后,不要忘了调用DeleteCriticalSection函数)

(4)在EnterCriticalSection函数中,仍有发生潜在的异常,如创建事件内核对象时,可能会抛出EXCEPTION_INVALID_HANDLE异常。要解决这个问题有两种方法,其一是用结构化异常处理来捕获错误。还有一种是选择InitializeCriticalSectionAndSpinCount来创建关键段,传将dwSpinCount最高位设为1,即告诉系统初始化关键段时就创建一个相关联的事件内核对象,如果无法创建该函数返回FALSE。

(5)注意死锁

  用临界区资源使多线程同步时候要特别注意线程死锁问题,假设程序有两临界资源(g_csA、g_csB)与两个子线程(子线程 A、子线程 B),子线程执行体流程如下图(图1)表示,当子线程 A 先获得临界资源 g_csA 后由于子线程 A 的时间片用完了,所以跳到子线程 B 进行执行,这时 B 将获得临界资源 g_csB,然后由于 A 获得临界资源 g_csA,所以 B 只好等待直至子线程B时间片用完,然后跳到子线程 A 继续执行,但是这时的临界资源 g_csB 已经被子线程 B占有,所以子线程 A 有进行等待直至时间片用完。于是子线程A与子线程B就进入了死锁现象流程如下图所示(图2)。

【CriticalSection程序】——模拟多线程对同一内存进行的各种操作(分配、释放、读、写)

#include <windows.h>
#include <tchar.h>
#include <locale.h>
#include <time.h> //////////////////////////////////////////////////////////////////////////
CRITICAL_SECTION g_cs = { };
DWORD* g_pdwAnyData = NULL;
//模拟对这个全局的数据指针进行分配、写入、释放等操作
void AllocData();
void WriteData();
void ReadData();
void FreeData(); //////////////////////////////////////////////////////////////////////////
DWORD WINAPI ThreadProc(PVOID pvParam);
#define THREADCOUNT 20
#define THREADACTCNT 20 //每个线程执行10次动作 int _tmain()
{
_tsetlocale(LC_ALL, _T("chs")); srand((unsigned int)time(NULL)); //初始化旋转式的临界区(旋转次数1024次,同时临界区内部创建事件内核对象)
if (!InitializeCriticalSectionAndSpinCount(&g_cs, 0x80000400))
return ; HANDLE aThread[THREADCOUNT];
DWORD dwThreadID = ;
SYSTEM_INFO si = { };
GetSystemInfo(&si); for (int i = ; i < THREADCOUNT;i++){
aThread[i] = CreateThread(NULL, , (LPTHREAD_START_ROUTINE)ThreadProc,
NULL,CREATE_SUSPENDED,&dwThreadID); //设置线程的亲缘性
SetThreadAffinityMask(aThread[i], << (i % si.dwNumberOfProcessors));
ResumeThread(aThread[i]);
} WaitForMultipleObjects(THREADCOUNT, aThread, TRUE, INFINITE); for (int i = ; i < THREADCOUNT;i++){
CloseHandle(aThread[i]);
} DeleteCriticalSection(&g_cs); //不要忘了删除临界区对象
if (NULL != g_pdwAnyData){
free(g_pdwAnyData);
g_pdwAnyData = NULL;
} return ;
} //线程函数
DWORD WINAPI ThreadProc(PVOID pvParam)
{
//为了增加冲突的可能性,让每个线程执行时,
//会随机分配20项任务(如分配内存、读取、释放等)
int iAct = rand() % ;
int iActCnt = THREADACTCNT; while (iActCnt--){
switch (iAct)
{
case :AllocData();break;
case :FreeData(); break;
case :WriteData(); break;
case :ReadData(); break;
} iAct = rand() % ;
}
return ;
} void AllocData()
{
EnterCriticalSection(&g_cs);
__try{
_tprintf(_T("线程[ID:0x%X] Alloc Data\n\t"), GetCurrentThreadId());
if (NULL == g_pdwAnyData){
g_pdwAnyData = (DWORD*)malloc(sizeof(DWORD));
_tprintf(_T("g_pdwAnyData为NULL,现己分配内存(Address:0x%08X)\n"),
g_pdwAnyData);
} else{
_tprintf(_T("g_pdwAnyData不为NULL,无法分配内存(Address:0x%08X)\n"),
g_pdwAnyData);
}
}
__finally{
LeaveCriticalSection(&g_cs);
}
} void WriteData()
{
EnterCriticalSection(&g_cs);
__try{
_tprintf(_T("线程[ID:0x%X] Write Data\n\t"), GetCurrentThreadId());
if (NULL != g_pdwAnyData){
*g_pdwAnyData = rand();
_tprintf(_T("g_pdwAnyData不为空,写入数据(%u)\n"),
*g_pdwAnyData);
} else{
_tprintf(_T("g_pdwAnyData为NULL,不能写入数据!\n"));
}
}
__finally{
LeaveCriticalSection(&g_cs);
}
} void ReadData()
{
EnterCriticalSection(&g_cs);
__try{
_tprintf(_T("线程[ID:0x%X] Read Data\n\t"), GetCurrentThreadId());
if (NULL != g_pdwAnyData){
_tprintf(_T("g_pdwAnyData不为空,读取数据(%u)\n"),
*g_pdwAnyData);
} else{
_tprintf(_T("g_pdwAnyData为NULL,不能读取数据!\n"));
}
}
__finally{
LeaveCriticalSection(&g_cs);
}
} void FreeData()
{
EnterCriticalSection(&g_cs);
__try{
_tprintf(_T("线程[ID:0x%X] Free Data\n\t"), GetCurrentThreadId());
if (NULL != g_pdwAnyData){
_tprintf(_T("g_pdwAnyData不为NULL,释放掉!\n"));
free(g_pdwAnyData);
g_pdwAnyData = NULL;
} else{
_tprintf(_T("g_pdwAnyData为NULL,不能释放!\n"));
}
}
__finally{
LeaveCriticalSection(&g_cs);
}
}

第8章 用户模式下的线程同步(2)_临界区(CRITICAL_SECTION)的更多相关文章

  1. Windows核心编程:第8章 用户模式下的线程同步

    Github https://github.com/gongluck/Windows-Core-Program.git //第8章 用户模式下的线程同步.cpp: 定义应用程序的入口点. // #in ...

  2. windows核心编程---第七章 用户模式下的线程同步

    用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...

  3. 第8章 用户模式下的线程同步(4)_条件变量(Condition Variable)

    8.6 条件变量(Condition Variables)——可利用临界区或SRWLock锁来实现 8.6.1 条件变量的使用 (1)条件变量机制就是为了简化 “生产者-消费者”问题而设计的一种线程同 ...

  4. 第8章 用户模式下的线程同步(3)_Slim读写锁(SRWLock)

    8.5 Slim读/写锁(SRWLock)——轻量级的读写锁 (1)SRWLock锁的目的 ①允许读者线程同一时刻访问共享资源(因为不存在破坏数据的风险) ②写者线程应独占资源的访问权,任何其他线程( ...

  5. 第8章 用户模式下的线程同步(1)_Interlocked系列函数

    8.1 原子访问:Interlocked系列函数(Interlock英文为互锁的意思) (1)原子访问的原理 ①原子访问:指的是一线程在访问某个资源的同时,能够保证没有其他线程会在同一时刻访问该资源. ...

  6. 《windows核心编程系列》七谈谈用户模式下的线程同步

    用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...

  7. 【windows核心编程】 第八章 用户模式下的线程同步

    Windows核心编程 第八章 用户模式下的线程同步 1. 线程之间通信发生在以下两种情况: ①    需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性 ②    一个线程需要通知其他线程 ...

  8. 用户模式下的线程同步的分析(Windows核心编程)

    线程同步 同一进程或者同一线程可以生成许多不同的子线程来完成规定的任务,但是多个线程同时运行的情况下可能需要对某个资源进行读写访问,比如以下这个情况:创建两个线程对同一资源进行访问,最后打印出这个资源 ...

  9. 《Windows核心编程》第八章——用户模式下的线程同步

    下面起了两个线程,每个对一个全局变量加500次,不假思索进行回答,会认为最后这个全局变量的值会是1000,然而事实并不是这样: #include<iostream> #include &l ...

随机推荐

  1. easyui日期在未加载easyui-lang-zh_CN.js出现英文的情况下加载中文的方法

    我们有时候在操作easyui的时候本来是加载了easyui-lang-zh_CN.js中文文件包,但是还是出现了英文.使得我们不得埋怨这框架咋这么不好用,其实我们仔细看看这个中文包就会发现里面很多都是 ...

  2. fullPage教程 -- 整屏滚动效果插件 fullpage详解

    1.引用文件 [html] view plain copy print?在CODE上查看代码片派生到我的代码片 <link rel="stylesheet" href=&qu ...

  3. <转>DevExpress使用经验总结

    DevExpress是一个比较有名的界面控件套件,提供了一系列的界面控件套件的DotNet界面控件.本文主要介绍我在使用 DevExpress控件过程中,遇到或者发现的一些问题解决方案,或者也可以所示 ...

  4. Autodesk招聘开发咨询顾问(北京或上海),需要内推的扔简历过来啊

    Autodesk现招聘两位二次开发技术顾问,为正式编制.享受所有Autodesk优越的福利资源(额外商业保险,公积金全部由公司支付,年度奖金,季度礼物, 节日礼物, 15天年假,不定期的培训...). ...

  5. 在eclipse中安装上genymotion插件

    1.安装genymotion-vbox,选择安装目录. 具体安装过程可见http://www.cnblogs.com/wuyudong/p/5601897.html   2.登录并创建模拟器   3. ...

  6. App开发流程之右滑返回手势功能

    iOS7以后,导航控制器,自带了从屏幕左边缘右滑返回的手势功能. 但是,如果自定义了导航栏返回按钮,这项功能就失效了,需要自行实现.又如果需要修改手势触发范围,还是需要自行实现. 广泛应用的一种实现方 ...

  7. C中的流程控制

    一. 流程控制 l 顺序结构:默认的流程结构.按照书写顺序执行每一条语句. l 选择结构:对给定的条件进行判断,再根据判断结果来决定执行哪一段代码. l 循环结构:在给定条件成立的情况下,反复执行某一 ...

  8. iOS 学习资源

    这份学习资料是为 iOS 初学者所准备的, 旨在帮助 iOS 初学者们快速找到适合自己的学习资料, 节省他们搜索资料的时间, 使他们更好的规划好自己的 iOS 学习路线, 更快的入门, 更准确的定位的 ...

  9. CocoaPods的安装和使用那些事(Xcode 7.2,iOS 9.2,Swift)

    Using The CocoaPods to Manage The Third Party Open-source Libaries 介绍 CocoaPods是用来管理你的Xcode项目的依赖库的.使 ...

  10. ThinkPHP 空方法 显示

    TP如果  一个控制器 没有一个方法 ,只要有一个模版,URL会对应显示模版名称. 例子 http://localhost/yiyunmap/map/test map控制器 并没有 test方法 但是 ...