WINDOWS下临界区的实现思路很简单,就是如果只有一个线程进入临界区的时候,只记录此线程的ID,让线程继续运行,如果此线程还没有离开临界区时又有另一个线程要进入临界区时,就生成一个内核对象,然后在此内核变量上进行等待,当线程退出时,把内核变量设置成受信状态,唤醒等待的线程。
实现方法可以分成两部分,一部分是进入线程的控制,在CRITICAL_SECTION结构中有一个变量表示进入线程循环进入的次数和所有等待进入的线程的总和,在线程进入和离开临界区时是需要对此变量进行互斥操作,WINDOWS是通过Lock汇编指令锁住总线,然后对此值进行操作来实现互斥的,此部分在网上有现成的源代码,可以找来看一下。
第二部分是内核对象操作的问题,临界区在内核对象创建时有两种方案,一种是提前创建内核变量,二是需要时动态创建,第一种情况没有什么好说的,第二种情况同样会存在多线程同时创建内核变量时而发生竞争的问题,因为对CRITICAL_SECTION结构中内核句柄替换的过程也是需要互斥的,WINDOWS用的是LOCK CMPXCHG指令来完成的,当发现结构中已有有效的句柄后,要把自己申请的内核对象关闭掉,然后在此内核对象上等待信号。当然,在线程离开临界区时也会先检查计数看是否有别的线程已经进入监界区了,如果有别的线程进入监界区了,那么要对CRITICAL_SECTION结构中的内核对象的句柄进行检查,如果句柄有效,那么直接设置受信状态,如果句柄无效,那么就说明另外进行的线程都在创建内核对象中,这时需要注意的是,需要离开的线程此时绝对不能直接离开,否则,后面的线程创建好内核对象后会一直等待下去(没有别的线程来设计内核对象的受信状态了),那么将要离开的线程需要做什么,一直等待下去?当然不是,此线程会也去创建内核变量,创建好后,用同样的方法去比较替换CRITICAL_SECTION结构中的内核句柄,如果已有线程先替换了,那么自己要关闭已申请的内核变量,然后把内核变量设置受信状态,离开。
临界区的几个问题:
1、 临界区的退出,不会检测是否是已经进入的线程,也就是说,我可以在A线程中调用进入临界区函数,在B线程调用退出临界区的函数,同样是成功;
2、 我在测试临界区的时候,如果我没有调用进入临界区的函数,直接退出的话,系统没有进行判断,但是计数发现了改变,此时此临界区就再也用不了了,因为结构中的数据已经乱掉了。
据说上面的两个问题,他的DEBUG版本里做了检查,我没有测试。
下面我自己模拟其机制写了一个很简单的临界区,为了简单我没有对线程ID进行记录,所以不允许线程循环进入,而且没有实现旋转锁的功能:
typedef class mutex_lock
{
public:
mutex_lock()
: LockCount(-1)
, hEvent(0)
{
}
~mutex_lock()
{
if(NULL != this->hEvent)
{
CloseHandle(this->hEvent);
}
this->hEvent = NULL;
this->LockCount = -1;
}
long GetLock();
long ReleaseLock();
private:
mutex_lock(mutex_lock&);
long LockCount;
HANDLE hEvent;
} MUTEXLOCK, *LPMUTEXLOCK;
long mutex_lock::GetLock()
{
__asm
{
mov ebx, [this]; // 把基址保存在ebx中
lock inc dword ptr [ebx]; // 对LockCount进行加锁加,保证多CPU时的唯一性,
je LRET1; // 如果LockCount加1为0的话,表示没有人在使用资源,同时利用上面的互斥加对资源进行占用。
cmp dword ptr [ebx+4], 0; // 此时LockCount加1大于0的情况,表示已有人使用此资源,要对此资源进行加内核锁
jne L1; // 如果平常没有创建内核锁,则进行创始一个
push 0;
push 0;
push 0;
push 0;
call dword ptr [CreateEvent];
mov edx, eax; // 创建内存锁成功后,在同样对hEvent变量进行互斥比较后替换,以处理多个线程同时对此值进行替换的情况,保证只有一个能够成功替换
mov eax, 0;
lock cmpxchg dword ptr [ebx+4], edx; // 互斥比较替换
je L1;
push edx; // 如果已经被别人替换过了,需要把自己创建的内核锁释放
call dword ptr [CloseHandle];
L1:
push INFINITE;
push [ebx+4];
call dword ptr [WaitForSingleObject]; // 在内核级进行等待
LRET1:
mov eax, 0
}
}
long mutex_lock::ReleaseLock()
{
__asm
{
mov ebx, [this]; // 把基址保存在ebx中
mov eax, -1;
lock xadd dword ptr [ebx], eax; // 进行交换自减交换操作,运行后,eax中是第一操作数先前的值,此值用会返回用来判断是否多调用了ReleaseLock
jl LRET2; // 没有别的线程占用资源,直接返回,用户可以通过返回值,分析是否失败
cmp dword ptr [ebx+4], 0; // 有别的线程在等待,检查内核锁,如果没有则进行创建
jne L2;
push 0;
push 0;
push 0;
push 0;
call dword ptr [CreateEvent];
mov edx, eax;
mov eax, 0;
lock cmpxchg dword ptr [ebx+4], edx; // 互斥替换内核锁句柄
je L2; // 已经有内核锁了,把自已申请的关闭
push edx;
call dword ptr [CloseHandle];
L2:
push [ebx+4]; // 设计信号,唤醒一个线程
call dword ptr [SetEvent];
mov eax, 0;
LRET2:
}
}