Windows核心编程笔记(八)用户模式下的线程同步
2017-01-10 07:06
369 查看
Long a; DWORD WINAPI ThreadFunc2(PVOID pvParam) { InterlockedExchangeAdd(&a,1); Return 0; }
第一部分 原子访问
引述自http://blog.csdn.net/ithzhang/article/details/8291027
系统中的线程必须访问系统资源,如堆、串口、文件、窗口以及其他资源。如果一个线程独占了对某个资源的访问,其他线程就无法完成工作。我们也必须限制线程在任何时刻都能访问任何资源。比如在一个线程读内存时要限制其他线程对此块内存进行写入。
线程之间的通信很重要,尤其是在以下两种情况下:
1:需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性。
2:一个线程需要通知其他线程某项任务已经完成。
线程同步包括许多方面,windows提供了许多基础设施使线程同步变得容易。
用户模式下的线程同步:方法一,原子访问
线程同步的一大部分与原子访问有关。所谓原子访问,指的是一个线程在访问某个资源的同时能够保证没有其他线程会在同一时刻访问统一资源。
比如有全局变量a=0;有两个线程同时对一全局变量进行a++操作,然后返回结果。那么最后a是多少呢?让我们从汇编代码上分析一下。
A++编译器会编译成两行代码:
Mov eax ,[a] Inc eax Mov [a],eax
如果一切正常的情况下,两个线程顺序执行:
线程A
Mov eax ,[a] Inc eax Mov [a],eax
线程B
Mov eax ,[a]
Inc eax
Mov [a],eax
a的值是2,这是毫无疑义的。
但是由于windows是抢占式操作系统,系统可能在任何时刻暂停执行一个线程。基于此,前面的代码可能会按下面的顺序执行:
线程A Mov eax,[a] Inc eax 线程B Mov eax,[a] Inc eax Mov [a],eax 线程B Mov [a],eax
如果按照这种顺序执行,a的值最终应该是1,而不是2。这看起来很不可思议。但是由于我们无法对windows的调度进行控制,在这样的一个环境编程会让程序员崩溃。
为了解决这个问题,我们需要一种方法能够保证对一个值的递增操作是原子操作。Interlocked系列函数提供了我们需要的解决方案。所有这些函数会以原子方式来操控一个值。
LONG InterlockedExchangeAdd( PLONG volatile plAddend, LONG lIncrement); LONGLONG InterlockedExchangeAdd64( PLONGLONG volatile pllAddend, LONGLONG llIncrement);
为上述两个函数传入一个长整形变量的地址和一个增量值,函数就会保证递增操作是以原子方式进行的。
前面的代码可以改成下面的代码:
改成上述代码后,对a的递增都是以原子方式进行。也就可以保证a的最终结果为2。如果只想以原子方式给一个值加1的话,可以使用InterlockedIncrement函数。
这些Interlock系列函数的工作机制取决于代码运行的cpu平台。如果是x86平台,那么Interlock函数会在总线上维持一个硬件信号,这个信号会阻止其他cpu访问同一个内存地址。
我们并不需要理解Interlock函数是如何工作的。无论编译器如何生成代码,无论机器上装了多少cpu,这些函数都能保证对值的修改是以原子方式进行的。
Interlock系列函数执行速度极快。通常只需要几个cpu周期,而且不需要在用户模式和内核模式进行切换。
使用这些函数也可以做减法,只要传入负值即可。
如果需要以原子方式执行变量交换可以调用:
LONG InterlockedExchange( PLONG volatile plTarget, LONG lValue); LONGLONG InterlockedExchange64( PLONGLONG volatile plTarget, LONGLONG lValue); PVOID InterlockedPointer( PVOID *volatile ppvTarget, PVOID pvValue);
InterloackedExchange和InterlockedExchangePointer会把第一个参数所指向的内存地址的当前值以原子方式替换为第二个参数指定的值
对于32位应用程序来说,它们都是替换的32位的值。而对于64位的应用程序InterlockedExchangePointer是替换的64位的值。因为此时指针也是64位的。
这两个函数都返回原来的值,在实现旋转锁时,InterlockedExchange函数非常有用。
Bool use=false; Void func() { While(InterlockedExchange(&use,true)==true) Sleep(0); //........ InterlockedExchange(&use,false); }
While循环不停的执行,把use的值设为true并检查原来的值是否为true。如果原来的值为false,则说明资源尚未被使用。于是将其设为使用中。如果原来的值是true则表明有其他线程正在使用该资源,调用sleep放弃该时间片的调度。
这里假定所有使用旋转锁的线程都是以相同的优先级运行的。但是对于单cpu的系统来说使用旋转锁是没有意义的。此处在检测到资源被占用时使用了sleep,可以睡眠一定随机的时间,这在一定程度上缓解了这种状况。
旋转锁假定被保护的资源只会被占用一段时间,与切换到内核模式然后等待相比,这种情况下以循环方式进行等待的效率会更高。许多开发人员会指定循环的次数,如果届时仍然无法访问资源,那么线程会切换到内核模式,并一直等待到资源可供使用为止。这就是关键段的使用方式。
在多处理器上使用旋转锁很有用,这是因为当一个线程在一个cpu上运行时,另一个线程可以再另一个cpu上循环等待。再次提醒,在单cpu的系统上循环锁毫无意义。
当cpu从内存中读取一个字节的时候,它并不是从内存中读取一个字节,而是取回一个高速缓存行。高速缓存行可能是32字节或是64字节。这取决于cpu。高速缓存行存在的目的是为了提高性能。这是根据程序的局部性原理。如果所有字节都在高速缓存行内那么cpu就不需要访问内存总线。
对于多处理器环境,高速缓存使得对内存的更新变得更加困难:
1:cpu1读取一个字节,这使得该字节以及与它相邻的字节被读取到cpu1的高速缓存中。
2:cpu2读取到同一字节,这使得该字节被读到cpu2的高速缓存中。
3:cpu1对内存中的这个字节进行修改,这使得该字节被写入到cpu1的告诉缓存中,并没有写回内存。
4:cpu2再次读取到同一字节。由于该字节已经在cpu2的高速缓存中,因此cpu2不需要在访问内存。但cpu2无法看到该字节在内存中的新值。
上述情况非常糟糕。但是cpu芯片的设计者早就考虑到了这种情况。当一个cpu修改了高速缓存行的一个字节时,机器中的其他cpu会收到通知,并将自己的高速缓存行作废。因此在刚才的情形中,cpu2的高速缓存就作废了。Cpu1必须将它的高速缓存写回内存,cpu2必须重新访问内存来填满它的高速缓存行。我们可以看到虽然高速缓存可以提高性能,但是在多处理器机器上它同样能够损伤性能。
为了提高性能,我们应该根据高速缓存的大小来将应用程序的数据组织在一起,将数据与缓存行的边界对齐。并把只读数据与可读写数据分别存放。
使用GetLogicalProcessorInformaiton函数可以获得cpu高速缓存行的大小。我们可以使用_declspec(align(#))指示符来对字段对齐加以控制。说了那么多的措施,其实最好的方法就是始终让一个cpu访问数据或只让一个线程访问数据就可以完全避免高速缓存行的问题。
如果我们只需要以原子方式修改一个值,那么Interlock系列函数非常好用。但是大多数情况下我们需要处理的数据结构往往要比一个简单的32位值或64位值复杂的多。为了能够以原子的方式来访问复杂的数据结构,我们可以使用windows使用的其他特性。
使用旋转锁不停的访问是非常糟糕的一种方式,最好的情况下就是当线程想要访问共享资源时,线程必须调用操作系统的一个函数,告诉操作系统自己等待什么资源。如果此时资源不可用,此线程将会进入等待状态,不可被调度。如果操作系统检测到资源已经可以使用了,系统就会将此线程设为可调度状态并访问此资源。
volatile关键字告诉编译器这个变量可能会被应用程序之外的其他东西修改,不要对该变量做任何形式的优化,而是始终从内存中读取此值。如果不对一个变量加上volatile限定符,编译器可能会对C++代码进行优化,如它会将变量值载入到寄存器中,以后的操作都是对寄存器中的值进行操作,并不会多次访问内存。对一个结构加上volatile限定符等于给结构中的所有成员都加入volatile限定符。
第二部分 关键代码段
关键段 (critical section)是一小段代码,它在执行之前需要独占对一些共享资源的访问权。这种方式可以让多行代码以“原子方式”来对资源进行操控。即,代码知道除了当前线程之外,没有任何线程会同时访问该资源。当然,系统仍然可以暂停当前线程去调度其它线程。但是,在当前线程离开关键段之前,系统是不会去调度任何想要访问同一资源的其它线程的。
一般情况下,我们会将CRITICAL_SECTION结构作为全局变量来分配,这样进程中的所有线程就能够非常方便地通过变量名来访问这些结构。在使用 CRIICAL_SECTION的时候,只有两个必要条件:第一条件是所有想要访问资源的线程必须知道用来保护资源的CRITICAL_SECTION结构的地址(我们可以通过自己喜欢的任何方式来把这个地址传给各个线程)。第二个条件是在任何线程试图访问被保护的资源之前,必须对 CRITICAL_SECTION结构的内部成员进行初始化。
下面这个函数用来对结构进行初始化:
VOID InitializeCriticalSection(PCRITICAL_SECTION pcs);
BOOL WINAPI InitializeCriticalSectionAndSpinCount( __out LPCRITICAL_SECTION lpCriticalSection, __in DWORD dwSpinCount);
当知道线程不再需要访问共享资源的时候,我们应该调用下面的函数来清理CRITICAL_SECTION结构: VOID DeleteCriticalSection(PCRITICAL_SECTION pcs);
然后我们在以下两个函数之间访问共享资源: VOID EnterCriticalSection(PCRITICAL_SECTION pcs); 。。。共享资源的访问。。。 VOID LeaveCriticalSection(PCRITICAL_SECTION pcs);
分析下 关键段是如何工作的,这里分析的代码来自React OS
CRITICAL_SECTION结构是这样的
typedef struct _CRITICAL_SECTION { PCRITICAL_SECTION_DEBUG DebugInfo; LONG LockCount; LONG RecursionCount; HANDLE OwningThread; HANDLE LockSemaphore; ULONG_PTR SpinCount; } CRITICAL_SECTION
初始化过程
BOOL WINAPI InitializeCriticalSectionAndSpinCount(OUT LPCRITICAL_SECTION lpCriticalSection, IN DWORD dwSpinCount) 带旋转锁的初始化 { NTSTATUS Status; /* Initialize the critical section */ Status = RtlInitializeCriticalSectionAndSpinCount((PVOID)lpCriticalSection, dwSpinCount); if (!NT_SUCCESS(Status)) { /* Set failure code */ BaseSetLastNTError(Status); return FALSE; } /* Success */ return TRUE;
VOID WINAPI InitializeCriticalSection(OUT LPCRITICAL_SECTION lpCriticalSection) 不带旋转锁的初始化 { NTSTATUS Status; /* Initialize the critical section and raise an exception if we failed */ Status = RtlInitializeCriticalSection((PVOID)lpCriticalSection); if (!NT_SUCCESS(Status)) RtlRaiseStatus(Status); } NTSTATUS NTAPI RtlInitializeCriticalSection(PRTL_CRITICAL_SECTION CriticalSection) { /* Call the Main Function */ return RtlInitializeCriticalSectionAndSpinCount(CriticalSection, 0); } NTSTATUS NTAPI RtlInitializeCriticalSectionAndSpinCount(PRTL_CRITICAL_SECTION CriticalSection, ULONG SpinCoun)t 两种初始化都由这一个函数完成
{ PRTL_CRITICAL_SECTION_DEBUG CritcalSectionDebugData; /* First things first, set up the Object */ DPRINT("Initializing Critical Section: %p\n", CriticalSection); CriticalSection->LockCount = -1; 注意这里是-1 CriticalSection->RecursionCount = 0; CriticalSection->OwningThread = 0; CriticalSection->SpinCount = (NtCurrentPeb()->NumberOfProcessors > 1) ? SpinCount : 0; //如果CPU是单核的使用0次循环等待,如果不是使用传入的 CriticalSection->LockSemaphore = 0; /* Allocate the Debug Data */ CritcalSectionDebugData = RtlpAllocateDebugInfo(); //申请一块用于调试数据的堆空间 如果申请失败,函数返回STATUS_NO_MEMORY,因此在内存不足的 DPRINT("Allocated Debug Data: %p inside Process: %p\n", 的机器上可能造成初始化的失败。参见InitializeCriticalSection的MSDN说明 CritcalSectionDebugData, NtCurrentTeb()->ClientId.UniqueProcess); if (!CritcalSectionDebugData) { /* This is bad! */ DPRINT1("Couldn't allocate Debug Data for: %p\n", CriticalSection); return STATUS_NO_MEMORY; } /* Set it up */ CritcalSectionDebugData->Type = RTL_CRITSECT_TYPE; CritcalSectionDebugData->ContentionCount = 0; CritcalSectionDebugData->EntryCount = 0; CritcalSectionDebugData->CriticalSection = CriticalSection; CritcalSectionDebugData->Flags = 0; CriticalSection->DebugInfo = CritcalSectionDebugData; 初始化调试数据 /* * Add it to the List of Critical Sections owned by the process. * If we've initialized the Lock, then use it. If not, then probably * this is the lock initialization itself, so insert it directly. */ if ((CriticalSection != &RtlCriticalSectionLock) && (RtlpCritSectInitialized)) { DPRINT("Securely Inserting into ProcessLocks: %p, %p, %p\n", &CritcalSectionDebugData->ProcessLocksList, CriticalSection, &RtlCriticalSectionList); /* Protect List */ RtlEnterCriticalSection(&RtlCriticalSectionLock); //在一个时间内只能有一个线程,将初始化的关键段对象加入到进程的 关键段对象链表中, /* Add this one */ InsertTailList(&RtlCriticalSectionList, &CritcalSectionDebugData->ProcessLocksList); /* Unprotect */ RtlLeaveCriticalSection(&RtlCriticalSectionLock); } else { DPRINT("Inserting into ProcessLocks: %p, %p, %p\n", &CritcalSectionDebugData->ProcessLocksList, CriticalSection, &RtlCriticalSectionList); /* Add it directly */ InsertTailList(&RtlCriticalSectionList, &CritcalSectionDebugData->ProcessLocksList); } return STATUS_SUCCESS; }
在这里 除了 CriticalSection->LockCount = -1; CriticalSection->SpinCount = (NtCurrentPeb()->NumberOfProcessors > 1) ? SpinCount : 0; 设置了循环次数
CritcalSectionDebugData->Type = RTL_CRITSECT_TYPE;
CritcalSectionDebugData->CriticalSection = CriticalSection;
CriticalSection->DebugInfo = CritcalSectionDebugData;
设置了调试数据结构 其他的都是初始化成了0
if ((CriticalSection != &RtlCriticalSectionLock) && (RtlpCritSectInitialized))
这里引用到了三个全局静态变量 RtlCriticalSectionLock是一个关键段对象的指针,该对象用来在线程把初始化好的关键段对象加入进程关键段对象链表中的时候做线程同步使用,RtlpCritSectInitialized 一个BOOL型的全局变量,表示是否已经初始化了 RtlCriticalSectionLock
RtlCriticalSectionList 则是一个全局的双向链表的表头,该链表用来存放进程所有的已初始化的 关键段对象
static LIST_ENTRY RtlCriticalSectionList;
链表的节点结构
typedef struct _LIST_ENTRY { struct _LIST_ENTRY *Flink; struct _LIST_ENTRY *Blink; } LIST_ENTRY,*PLIST_ENTRY;
关键段调试信息结构
typedef struct _RTL_CRITICAL_SECTION_DEBUG { WORD Type; WORD CreatorBackTraceIndex; struct _RTL_CRITICAL_SECTION *CriticalSection; LIST_ENTRY ProcessLocksList; DWORD EntryCount; DWORD ContentionCount; DWORD Flags; WORD CreatorBackTraceIndexHigh; WORD SpareWORD; }
这样就可以根据表头找到对应CritcalSectionDebugData->ProcessLocksList 然后根据它在结构体内的地址使用偏移量定位其他数据,PLIST_ENTRY+4 就是关键段对象的指针 struct _RTL_CRITICAL_SECTION *CriticalSection;
EnterCriticalSection 做了些什么
它调用了 RtlEnterCriticalSection
NTSTATUS NTAPI RtlEnterCriticalSection(PRTL_CRITICAL_SECTION CriticalSection) { HANDLE Thread = (HANDLE)NtCurrentTeb()->ClientId.UniqueThread; if (InterlockedIncrement(&CriticalSection->LockCount) != 0) 如果+1之后锁定次数不为0, 刚初始化完的是-1 这里循环不执行,如果是有线程在占用执行循环 { if (Thread == CriticalSection->OwningThread) 如果是本线程占用 { CriticalSection->RecursionCount++; 资源引用+1 return STATUS_SUCCESS; } RtlpWaitForCriticalSection(CriticalSection); //执行等待 } CriticalSection->OwningThread = Thread; 将拥有线程设置为本线程 CriticalSection->RecursionCount = 1; 资源引用设置为1 return STATUS_SUCCESS; }
在来看看RtlpWaitForCriticalSection
创建事件内核对象
NTAPI RtlpCreateCriticalSectionSem(PRTL_CRITICAL_SECTION CriticalSection) { HANDLE hEvent = CriticalSection->LockSemaphore; HANDLE hNewEvent; NTSTATUS Status; if (!hEvent) { Status = NtCreateEvent(&hNewEvent, EVENT_ALL_ACCESS,NULL, SynchronizationEvent,FALSE); if (!NT_SUCCESS(Status)) {hNewEvent = INVALID_HANDLE_VALUE; } if (InterlockedCompareExchangePointer((PVOID*)&CriticalSection->LockSemaphore,(PVOID)hNewEvent,NULL) != NULL) { if (hEvent != INVALID_HANDLE_VALUE) { DPRINT("Closing already created event: %p\n", hNewEvent); NtClose(hNewEvent); } } } return; }
RtlpWaitForCriticalSection 执行等待
NTSTATUS NTAPI RtlpWaitForCriticalSection(PRTL_CRITICAL_SECTION CriticalSection) { NTSTATUS Status; EXCEPTION_RECORD ExceptionRecord; BOOLEAN LastChance = FALSE; /* Do we have an Event yet? */ if (!CriticalSection->LockSemaphore) { RtlpCreateCriticalSectionSem(CriticalSection); 如果事件内核对象没有创建,用上面的函数创建它 } if (CriticalSection->DebugInfo) CriticalSection->DebugInfo->EntryCount++; 增加等待线程的个数 if (LdrpShutdownInProgress && LdrpShutdownThreadId == NtCurrentTeb()->RealClientId.UniqueThread) { DPRINT("Forcing ownership of critical section %p\n", CriticalSection); CriticalSection->LockCount = 0; return STATUS_SUCCESS; } 如果进程正在关闭,解锁 for (;;) { if (CriticalSection->DebugInfo) CriticalSection->DebugInfo->ContentionCount++; if (CriticalSection->LockSemaphore == INVALID_HANDLE_VALUE) 如果创建事件对象失败 使用系统的keyed event等待 { Status = NtWaitForKeyedEvent(NULL, CriticalSection, FALSE, &RtlpTimeout); } else { Status = NtWaitForSingleObject(CriticalSection->LockSemaphore, 使用内核事件对象 等待 FALSE, &RtlpTimeout); } RtlpTimeout是一个全局的等待时间,书上说是3天 if (Status == STATUS_TIMEOUT) { /* Is this the 2nd time we've timed out? */ if (LastChance) { ERROR_DBGBREAK("Deadlock: 0x%p\n", CriticalSection); /* Yes it is, we are raising an exception */ ExceptionRecord.ExceptionCode = STATUS_POSSIBLE_DEADLOCK; ExceptionRecord.ExceptionFlags = 0; ExceptionRecord.ExceptionRecord = NULL; ExceptionRecord.ExceptionAddress = RtlRaiseException; ExceptionRecord.NumberParameters = 1; ExceptionRecord.ExceptionInformation[0] = (ULONG_PTR)CriticalSection; RtlRaiseException(&ExceptionRecord); } /* One more try */ LastChance = TRUE; 如果时间超时抛出异常 } else { /* If we are here, everything went fine */ return STATUS_SUCCESS; 有信号了返回 } } }
这里React OS 在用事件对象等待之前,没有使用旋转锁 循环尝试获取执行权,毕竟它不是真正的Windwos
再看 LeaveCriticalSection 对应的是 RtlLeaveCriticalSection(PRTL_CRITICAL_SECTION CriticalSection)
NTSTATUS NTAPI RtlLeaveCriticalSection(PRTL_CRITICAL_SECTION CriticalSection) { #if DBG HANDLE Thread = (HANDLE)NtCurrentTeb()->ClientId.UniqueThread; if (Thread != CriticalSection->OwningThread) { return STATUS_INVALID_PARAMETER; //如果本线程没有处于拥有关键段对象的状态,调用无效。 } #endif if (--CriticalSection->RecursionCount) //如果线程多次调用EnterCriticalSection,就需要多次调用LeaveCriticalSection { InterlockedDecrement(&CriticalSection->LockCount); 每次LockCount减1 } else { //在LockCount减去为 -1时 表示可以让其他线程拥有而结束等待了 CriticalSection->OwningThread = 0; if (-1 != InterlockedDecrement(&CriticalSection->LockCount)) { RtlpUnWaitCriticalSection(CriticalSection); //结束等待 } } return STATUS_SUCCESS; }
RtlpUnWaitCriticalSection(PRTL_CRITICAL_SECTION CriticalSection) { NTSTATUS Status; *******************************这里省略一些检查不贴了 if (CriticalSection->LockSemaphore == INVALID_HANDLE_VALUE) { /* Release keyed event */ Status = NtReleaseKeyedEvent(NULL, CriticalSection, FALSE, &RtlpTimeout); } else { /* Set the event */ Status = NtSetEvent(CriticalSection->LockSemaphore, NULL); } 重置事件状态为有信号 *******************************************// 省略返回信息 }
总结:
1、初始化时将,LockCount设置为-1,SpinCount如果是单CPU设置为0,如果不是单CPU 调用InitializeCriticalSection设置为0,调用InitializeCriticalSectionAndSpinCount设置为参数指定的值。
2、每次调用EnterCriticalSection将LockCount +1,如果给其加1后为0表示拥有对象函数返回 并使RecursionCoun为1,OwningThread设置为本线程
如果LockCount +1后不为0,表示对象被其他线程拥有,进入等待函数,使用旋转锁和事件对象等待至到其他线程放弃对象拥有权。
注意在线程在拥有对象后重复调用 EnterCriticalSection时,RecursionCoun和LockCount 同时+1,RecursionCoun总是大于LockCount 一个数。
3、在调用LeaveCriticalSection 时,首先会检查是否本线程拥有对象,如果不是直接返回。
递减RecursionCoun和LockCount 知道前者等于0后者等于-1,然后调用事件重置函数 将事件设置为有信号状态,其他等待线程有个被唤醒。
4、DebugInfo 记录了统计信息。
最后 还有DeleteCriticalSection 和 TryEnterCriticalSection 也贴一下
RtlDeleteCriticalSection(PRTL_CRITICAL_SECTION CriticalSection) { NTSTATUS Status = STATUS_SUCCESS; if (CriticalSection->LockSemaphore) { Status = NtClose(CriticalSection->LockSemaphore); //关闭事件对象 } RtlEnterCriticalSection(&RtlCriticalSectionLock); if (CriticalSection->DebugInfo) { RemoveEntryList(&CriticalSection->DebugInfo->ProcessLocksList); } RtlLeaveCriticalSection(&RtlCriticalSectionLock); //从进程关键段对象链表上移除该对象 if (CriticalSection->DebugInfo) { RtlpFreeDebugInfo(CriticalSection->DebugInfo); //释放调试信息用的堆内存 } RtlZeroMemory(CriticalSection, sizeof(RTL_CRITICAL_SECTION));//将对象内存全部置0 return Status; }
BOOLEAN NTAPI RtlTryEnterCriticalSection(PRTL_CRITICAL_SECTION CriticalSection) { if (InterlockedCompareExchange(&CriticalSection->LockCount, 0, -1) == -1) 如果LockCount等于-1 将其设置为0 { CriticalSection->OwningThread = NtCurrentTeb()->ClientId.UniqueThread; CriticalSection->RecursionCount = 1; 设置该线程拥有对象,并返回TRUE return TRUE; } else if (CriticalSection->OwningThread == NtCurrentTeb()->ClientId.UniqueThread)//如果线程已经拥有对象RecursionCoun和LockCount都+1 返回TRUE { InterlockedIncrement(&CriticalSection->LockCount); CriticalSection->RecursionCount++; return TRUE; } return FALSE; //不然返回FALSE }
这里可以看到每个TryEnterCriticalSection 都必须 对应一个LeaveCriticalSection
相关文章推荐
- Windows核心编程笔记(6)----用户模式下的线程同步
- Windows核心编程笔记(八)用户模式下的线程同步 SRWLock剖析
- 《Windows via C/C++》学习笔记 —— 用户模式的“线程同步”之“读写锁”
- Windows Via C/C++ 读书笔记 5 用户模式的线程同步
- 第八章:用户模式下的线程同步
- 用户模式下线程同步之关键段
- 用户模式下的线程同步
- 《Windows via C/C++》学习笔记 —— 用户模式的“线程同步”之“互锁函数族”
- 用户模式下线程同步(一)
- Windows via C/C++ 学习(16)用户模式下的线程同步(一)
- 《Windows核心编程》第8章 用户模式下的线程同步
- Chapter08-用户模式下的线程同步
- 用户模式的线程同步原子访问:InterLocked互锁函数
- Windows Via C/C++:用户模式下的线程同步——临界区 Critical Sections
- Windows Via C/C++:用户模式下的线程同步——Cache行
- 用户模式下的线程同步
- 《Windows via C/C++》学习笔记 —— 用户模式的“线程同步”之“条件变量”
- Windows Via C/C++:用户模式下的线程同步——原子操作:Interlocked函数族
- Windows Via C/C++:用户模式下的线程同步——Condition Variables 条件变量
- Windows核心编程:用户模式下的线程同步