第8章 用户模式下的线程同步(2)_临界区(CRITICAL_SECTION)
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)的更多相关文章
- Windows核心编程:第8章 用户模式下的线程同步
Github https://github.com/gongluck/Windows-Core-Program.git //第8章 用户模式下的线程同步.cpp: 定义应用程序的入口点. // #in ...
- windows核心编程---第七章 用户模式下的线程同步
用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...
- 第8章 用户模式下的线程同步(4)_条件变量(Condition Variable)
8.6 条件变量(Condition Variables)——可利用临界区或SRWLock锁来实现 8.6.1 条件变量的使用 (1)条件变量机制就是为了简化 “生产者-消费者”问题而设计的一种线程同 ...
- 第8章 用户模式下的线程同步(3)_Slim读写锁(SRWLock)
8.5 Slim读/写锁(SRWLock)——轻量级的读写锁 (1)SRWLock锁的目的 ①允许读者线程同一时刻访问共享资源(因为不存在破坏数据的风险) ②写者线程应独占资源的访问权,任何其他线程( ...
- 第8章 用户模式下的线程同步(1)_Interlocked系列函数
8.1 原子访问:Interlocked系列函数(Interlock英文为互锁的意思) (1)原子访问的原理 ①原子访问:指的是一线程在访问某个资源的同时,能够保证没有其他线程会在同一时刻访问该资源. ...
- 《windows核心编程系列》七谈谈用户模式下的线程同步
用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...
- 【windows核心编程】 第八章 用户模式下的线程同步
Windows核心编程 第八章 用户模式下的线程同步 1. 线程之间通信发生在以下两种情况: ① 需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性 ② 一个线程需要通知其他线程 ...
- 用户模式下的线程同步的分析(Windows核心编程)
线程同步 同一进程或者同一线程可以生成许多不同的子线程来完成规定的任务,但是多个线程同时运行的情况下可能需要对某个资源进行读写访问,比如以下这个情况:创建两个线程对同一资源进行访问,最后打印出这个资源 ...
- 《Windows核心编程》第八章——用户模式下的线程同步
下面起了两个线程,每个对一个全局变量加500次,不假思索进行回答,会认为最后这个全局变量的值会是1000,然而事实并不是这样: #include<iostream> #include &l ...
随机推荐
- window7安装git详解
1.Git详细介绍 一.Git的诞生 Linus虽然创建了Linux,但Linux的壮大是靠全世界热心的志愿者参与的,这么多人在世界各地为Linux编写代码,那Linux的代码是如何管理的呢? 事实是 ...
- swift学习笔记之-可选链式调用
//可选链式调用 import UIKit /*可选链式调用(Optional Chaining) 1.在可选值上请求和调用该可选值的属性.方法及下标的方法,如果可选值有值,那么调用就会成功,返回可选 ...
- jquery原型方法map的使用和源码分析
原型方法map跟each类似调用的是同名静态方法,只不过返回来的数据必须经过另一个原型方法pushStack方法处理之后才返回,源码如下: map: function( callback ) { re ...
- jQuery绑定事件的四种方式
jQuery提供了多种绑定事件的方式,每种方式各有其特点,明白了它们之间的异同点,有助于我们在写代码的时候进行正确的选择,从而写出优雅而容易维护的代码.下面我们来看下jQuery中绑定事件的方式都 ...
- 【Bootstrap】4.企业网站(待续)
上一章有队个人站点站点进行一些优化.本章,轮到我们充实这个作品站点了,补充一些项目,从而展示我们的能力.话句话说,我们要构建一个相对复杂的企业网站主页. 下面有几个成功企业的网站: □ Zappos ...
- ID卡和IC卡
1.ID卡 ID卡就是一种身份识别卡,卡内除了卡号之外,无任何加密功能. ID卡的工作原理:它是由卡.读卡器.后台控制器组成的. (1)读卡器通过天线发射射频信号 (2)当卡进入信号范围内后卡被激活 ...
- 好推二维码如何通过应用宝微下载支持微信自动打开APP下载?
好推二维码 官网 http://www.hotapp.cn 1. 为什么使用应用宝微下载? APP下载二维码,通过微信扫描下载的时候,微信目前只支持应用宝微下载,才能在微信里直接打开下载,否则就需要在 ...
- Android - ADB 的使用
一.什么是ADB? 1.ADB全称Android Debug Bridge, 是android sdk里的一个工具,用这个工具可以直接操作管理android模拟器或者真实的andriod设备 2.AD ...
- Android源码笔记——Camera系统架构
Camera的架构与Android系统的整体架构保持一致,如下图所示,本文主要从以下四个方面对其进行说明. Framework:Camera.java Android Runtime:android_ ...
- js 事件处理程序 事件对象
事件:用户或浏览器自身执行的动作: 事件处理程序:响应某个事件的函数: 事件流:从页面中接收事件的顺序. 1.DOM事件流 "DOM2级事件"规定的事件流包括三个阶段:事件捕获阶段 ...