Win32多线程编程(3) — 线程同步与通信

时间:2022-12-08 04:03:33
 

一.线程间数据通信

系统从进程的地址空间中分配内存给线程栈使用。新线程与创建它的线程在相同的进程上下文中运行。因此,新线程可以访问进程内核对象的所有句柄、进程中的所有内存以及同一个进程中其他所有线程的栈。这样一来,同一个进程中的多个线程可以很容易的相互通信。

到目前为止,将数据从一个线程传到另一个线程的惟一方法是在创建线程时传递给新线程一个指针参数(LPVOID lpParam)。参数lpParam为LPVOID指针类型,我们可在其中存储普通的数值(size为平台地址总线宽度),也可以存放指向某个数据结构(struct或class)的地址。在新线程函数中,解引用时需要强制类型转换回原类型,以进行正确的访问。

以下代码段演示了一个典型的多线程场景。

// A typical multithread scene

DWORD WINAPI FirstThread(PVOID lpParam)

{

// Initialize a stack-based variable

int x = 0;

DWORD dwThreadID;

// Create a new thread.

HANDLE hThread = CreateThread(NULL, 0, SecondThread, (LPVOID)&x, 0, &dwThreadID);

// We don't reference the new thread anymore,

// so close our handle to it.

CloseHandle(hThread);

// Our thread is done.

// BUG:our stack will be destroyed,

// but SecondThread might try to access it.

return 0;

}

DWORD WINAPI SecondThread(LPVOID lpParam)

{

// Do some lengthy processing here.

// ...

// Attempt to access the variable on FirstThread's stack.

// NOTE:This may cause an access violation - it depends on timing!

*((int*)lpParam) = 5;

// ...

return 0;

}

上述场景中,Windows没有维持线程之间的“父子关系“,即父线程FirstThread已经终止运行,而子线程SecondThread仍在继续运行。以上父子关系只是为了一种解说上的方便,实际上FirstThread和SecondThread具有相同的优先级(默认是 normal),因此它们“同时”执行。这样,FirstThread在开辟SecondThread后,不等SecondThread运行至代码*((int*)lpParam) = 5;即可能退出。FirstThread栈上的自动变量x已销毁,而SecondThread试图去访问之,将导致Access Violation。这是多线程编程中新手常犯的错误。

解决以上问题,大概有以下三种方案。

(1)让创建线程等待新线程退出后才退出。在FirstThread中代码CloseHandle(hThread);之前WaitForSingleObject(hThread, INFINITE);这样保证SecondThread中对FirstThread栈中自动变量x的访问有效期。

(2)将x声明为堆变量,即int *px = new int;,在SecondThread中对px进行访问完毕后调用delete px;释放堆内存。由于堆内存对进程有效,因此,上述代码中FirstThread先退出,在SecondThread中对px的访问依然有效,直到进程的某处将该内存delete掉。这是在需要动态创建线程参数(数据结构)时的一种解决方案,实际应用中经常用到。

(3)将x声明为静态变量static int x = 0;则将存储在静态存储区域。这里有全局和局部之分,若在FirstThread之前声明,则整个程序均可显式访问x;若在FirstThread之中声明,则x只在FirstThread中可见。当然这里传址给SecondThread,SecondThread可按址访问。

在方案(3)中,若在FirstThread之中将x声明为静态变量,将使函数SecondThread不可重入。换言之,不能创建两个使用相同线程函数的线程,因为这两个线程将共享同一个静态变量。这涉及到下文将要阐述的线程同步问题。

二.多线程同步互斥问题

1.同步问题的导入

多个线程共享数据时,同时读没有问题,但如果同时读和写,情况就不同了。

在本次线程内, 读取一个变量时,为提高存取速度,编译器优化时,有时会先把变量读取到一个寄存器中;以后取变量值时,就直接从寄存器中取值;当变量值在本线程中改变时,会同时把变量的新值拷贝到该寄存器中,以便保持一致;当变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致。

// CountError

#include <stdio.h>

#include <windows.h>

#include <process.h>

int g_nCount1 = 0;

int g_nCount2 = 0;

BOOL g_bContinue = TRUE;

UINT __stdcall ThreadFunc(LPVOID);

int main(int argc, char* argv[])

{

UINT uId;

HANDLE h[2];

h[0] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);

h[1] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);

// 等待1秒后通知两个计数线程结束,关闭句柄

Sleep(1000);

g_bContinue = FALSE;

// 等待两个线程都运行完

::WaitForMultipleObjects(2, h, TRUE, INFINITE);

::CloseHandle(h[0]);

::CloseHandle(h[1]);

printf("g_nCount1 = %d /n", g_nCount1);

printf("g_nCount2 = %d /n", g_nCount2);

return 0;

}

UINT __stdcall ThreadFunc(LPVOID)

{

while(g_bContinue)

{

g_nCount1++;

g_nCount2++;

}

return 0;

}

以上代码中线程h[0]和h[1](具有相同的线程函数ThreadFunc)同时增加全局变量g_nCount1和g_nCount2的计数。按道理来说最终在主线程中输出的它们的值应该是相同的,可是结果却并不尽如人意。

上述测试中,g_nCount1和g_nCount2的值往往并不相同。出现此种结果主要是因为同时访问g_nCount1和g_nCount2的两个线程具有相同的优先级。在执行过程中,如果第一个线程取走g_nCount1的值准备进行自加操作的时候,它的时间片恰好用完,系统切换到第二个线程去对g_nCount1进行自加操作;一个时间片过后,第一个线程再次被调度,此时它会将上次取出的值自加,并放入g_nCount1所在的内存里,这就会覆盖掉第二个线程对g_nCount1的自加操作。变量g_nCount2也存在相同的问题。由于这样的事情的发生次数是不可预知的,所以最终它们的值就不相同了。

针对以上问题,可使用volatile修饰静态变量去优化,直接存取原始内存地址。对于volatile变量,优化器在用到这个变量时每次都必须小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。这对于经常同硬件、中断、RTOS等等打交道的嵌入式系统程序而言,是一种很好的解决方案。但常规情况下,很少使用去优化的volatile方式。

上例中,g_nCount1 和g_nCount2是全局变量,属于该进程内所有线程共有的资源。解决问题的关键在于在一个线程对某个对象进行操作的过程中,需要有某种机制阻止其他线程的操作,这将涉及到到同步、互斥等话题。

2.同步与互斥的概念

同步与互斥往往像一对孪生兄弟,总是在同一语境中被提及,但往往语焉不详。下面厘清一下它们之间的暧昧。

互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。

大街上的移动厕所往往只有一个茅坑,如果把厕所(茅坑)当做一种资源,则某一时刻,这种珍贵的资源只允许一人享用。这种对资源使用的独占性和排他性即互斥。上述上厕所场景中,互斥这一原则的约束下,内急者先来先上,维持了良好的公共秩序。

从同步的制约性要素考虑,上厕所行为不存在“同步”问题。因为A上完厕所即可走人,接下来轮到B,B进行与A几乎完全一样的独立操作,A和B也许素昧平生,它们之间不存在任何的制约关系。

关于同步的典型案例是“生产者-消费者”模型。生产者占用缓冲区时,消费者不能占用,反之亦然,这个即互斥;消费者必须要等生产者生产之后,才能消费,这个即同步。在这里,同步与互斥形影相随,同步中暗含互斥。同时,可以看出,生产者和消费者的同步关系本质上是一种供需制约关系。

3.Win32多线程同步策略

多线程同步就要保证在一个线程占有公共资源的时候,其他线程不会再次占有这个资源。所以,解决同步问题,就是保证整个存取过程的独占性。同步可以保证在一个时间内只有一个线程对某个共享资源有控制权,其本质是微观串行所体现出来的等待。

之前谈到的那个计数错误(Count Error)问题,涉及到如何协调线程间的活动,以保证对资源的正确访问。Windows操作系统提供了多种同步手段,同步对象包括临界区(Critical Section)、事件(Event)、信号量(Semaphore)、互斥量(Mutex)等。

(1)  临界区对象(CRITICAL_SECTION),也称关键代码段

临界区对象依赖一个CRITICAL_SECTION数据结构记录一些信息,确保在同一时间只有一个线程访问该数据段中的数据。

使用临界区实施同步,首先需要声明一个CRITICAL_SECTION 结构。对CRITICAL_SECTION 结构的操作包括Initialize、Enter/Leave、Delete。

编程的时候,要把临界区对象定义在想保护的数据段中,然后在任何线程使用此临界区对象之前,调用InitializeCriticalSection函数对它进行初始化。

// The InitializeCriticalSection function initializes a critical section object.

VOID InitializeCriticalSection(

LPCRITICAL_SECTION lpCriticalSection  // critical section

);

之后,线程访问临界区中数据的时候,必须首先调用 EnterCriticalSection 函数,申请进入临界区。在同一时间内,Windows只允许一个线程进入临界区。所以在申请的时候,如果有另一个线程在临界区的话,EnterCriticalSection函数会一直等待下去,直到其他线程离开临界区才返回。

// The EnterCriticalSection function waits for ownership of the specified critical section object. The function returns when the calling thread is granted ownership.

VOID EnterCriticalSection(

LPCRITICAL_SECTION lpCriticalSection  // critical section

);

当操作完成的时候,要调用LeaveCriticalSection函数将临界区交还给Windows系统,以便其他线程可以申请使用。否则,就是占着茅坑不拉屎憋死其他人的不道德行为。

// The LeaveCriticalSection function releases ownership of the specified critical section object.

VOID LeaveCriticalSection(

LPCRITICAL_SECTION lpCriticalSection   // critical section

);

当程序不再使用临界区对象的时候,必须使用DeleteCriticalSection函数执行删除操作,释放资源。

// The DeleteCriticalSection function releases all resources used by an unowned critical section object.

VOID DeleteCriticalSection(

LPCRITICAL_SECTION lpCriticalSection   // critical section

);

声明一个CRITICAL_SECTION对象,即*修建了一座厕所。厕所这一资源起初是上锁的,Initialize可看做厕所开锁对外开放,使厕所可用。Enter可看做上厕所的排队过程,一旦茅坑空出,即可持票进入享用;Leave可看做如厕完毕冲水走人。厕所被很多人Enter/Leave用了几年后,其使命结束,*依据城市规划,将其拆掉—Delete收回。

临界区对象能够很好地保护共享数据,但是它不能够用于进程之间资源的锁定。由于它不是内核对象,故临界区只能用于在同一进程内的线程同步。如果要在进程间维持线程的同步,可以使用事件内核对象。

(2)       事件内核对象(event

多线程程序设计大多会涉及线程间相互通信。主线程在创建工作线程的时候,可以通过参数给工作线程传递初始化数据,当工作线程开始运行后,还需要通过通信机制来控制工作线程。同样,工作线程有时候也需要将一些情况主动通知主线程。事件内核对象是一种比较好的通信方法。

事件对象(event)是一种抽象的对象,它也有未受信(nonsignaled)和受信(signaled)两种状态。编程人员也可以使用WaitForSingleObject/WaitForMultipleObjects函数等待其变成受信状态。不同于其他内核对象,系统提供的一些API可以使事件对象在这两种状态之间转化。可以把事件对象看成是一个设置在Windows 内部的标志,它的状态设置和测试工作由Windows来完成。

事件对象包含 3 个成员:nUsageCount(使用计数)、bManualReset(是否人工重置)和bSignaled(是否受信)。成员nUsageCount记录了当前的使用计数,当使用计数为 0 的时候,Windows 就会销毁此内核对象占用的资源;成员bManualReset指定在一个事件内核对象上等待的函数返回之后,Windows是否重置这个对象为未受信状态;成员bSignaled 指定当前事件内核对象是否受信。下面要介绍的操作事件内核对象的函数会影响这些成员的值。

要使用事件对象,首先用CreateEvent函数去创建它。

// The CreateEvent function creates or opens a named or unnamed event object.

HANDLE CreateEvent(

LPSECURITY_ATTRIBUTES lpEventAttributes, // SD

BOOL bManualReset,                       // reset type

BOOL bInitialState,                      // initial state

LPCTSTR lpName                           // object name

);

参数一为事件对象的安全属性,一般填充NULL表示取默认值。

参数二,选择事件对象的重置方式以决定类型。bManualReset = TRUE则表示人工重置(manual-reset);bManualReset = FALSE则表示自动重置(auto-reset)。当一个人工重置的事件对象受信以后,所有等待在这个事件上的线程都会变为可调度状态;当一个自动重置的事件对象受信以后,Windows 仅允许一个等待在该事件上的线程变成可调度状态,然后就自动重置此事件对象为未受信状态。通常使用自动重置的事件内核对象,即设置bManualReset =FALSE。

参数三bInitialState对应着 bSignaled 成员的初态。若将它设为TRUE,则表示事件对象创建时的初始状态为受信;若将它设为FALSE,则初始状态为未受信。通常设置初始状态为未受信,即置bInitialState = FALSE。

参数四指定事件对象的名称,以便跨进程按名访问。

跨进程访问事件内核对象,可传入事件对象名调用OpenEvent获取该对象的句柄。

// The OpenEvent function opens an existing named event object.

HANDLE OpenEvent(

DWORD dwDesiredAccess,  // access

BOOL bInheritHandle,    // inheritance option

LPCTSTR lpName          // object name

);

系统创建或打开一个事件内核对象后,会返回事件的句柄。当编程人员不使用此内核对象的时候,应该调用CloseHandle函数释放它占用的资源。

事件对象被建立后,程序可以通过SetEvent和ResetEvent函数来设置它的状态。

// The SetEvent function sets the specified event object to the signaled state.

BOOL SetEvent(

HANDLE hEvent   // handle to event

);

// The ResetEvent function sets the specified event object to the nonsignaled state.

BOOL ResetEvent(

HANDLE hEvent   // handle to event

);

与SetEvent/ResetEvent相关的另一个函数是PulseEvent。顾名思义,所谓PulseEvent即瞬间Set/Reset,至于这个瞬间多长及其效用,这里不详解。可参考《关于线程同步 PulseEvent()

// The PulseEvent function sets the specified event object to the signaled state and then resets it to the nonsignaled state after releasing the appropriate number of waiting threads.

BOOL PulseEvent(

HANDLE hEvent   // handle to event object

);

事件内核对象的同步,代码上体现在对WaitForSingleObject/WaitForMultipleObjects函数的调用,等待事件对象的置信。我们可以将之想象为厕所门外的“有人/没人”信号灯,红灯有人(nonsignaled),绿灯无人(signaled)。

事件对象是一个用于线程间通信被广泛使用的内核对象。因为它是一个内核对象,所以也可以跨进程使用。依靠在线程间通信就可以使各线程的工作协调进行,达到同步的目的。

(3)      信号量内核对象(Semaphore

信号量内核对象对线程的同步方式与前面几种不同,它允许多个线程在同一时刻访问某一资源,但是需要限制同一时刻访问此资源的最大线程数目。

首先使用CreateSemaphore函数创建信号量内核对象。

// The CreateSemaphore function creates or opens a named or unnamed semaphore object.

HANDLE CreateSemaphore(

LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // SD

LONG lInitialCount,          // initial count

LONG lMaximumCount,          // maximum count

LPCTSTR lpName                // object name

);

CreateSemaphore函数创建信号量时,参数三指定允许的最大资源计数,参数二指定当前可用的初始资源计数。一般将lInitialCount设置与lMaximumCount相等。

参数四即内核对象名称,以便跨进程执行OpenSemaphore按名访问。

只要当前可用资源计数大于0,就可以发出信号量信号,在该信号量上的等待函数调用WaitForSingleObject返回。每增加一个线程对共享资源的访问,当前可用资源计数就会减1。WaitForSingleObject返回后,调用线程在对共享资源的同步处理完毕后,应调用ReleaseSemaphore来增加当前可用资源计数。否则,将会出现当前正在处理共享资源的实际线程并没有达到要限制的数值,而其他线程却因为当前可用资源计数为0而仍无法进入的情况。

// The ReleaseSemaphore function increases the count of the specified semaphore object by a specified amount.

BOOL ReleaseSemaphore(

HANDLE hSemaphore,       // handle to semaphore

LONG lReleaseCount,      // count increment amount

LPLONG lpPreviousCount   // previous count

);

参数一为信号量内核对象句柄;参数二为计数递增值,一般设为1,当然也可以按需要设置大于1的值;参数三为之前的计数,往往填NULL,当然可指定导出到当地变量。

信号量内核对象的同步,代码上体现在对WaitForSingleObject/WaitForMultipleObjects函数的调用,其同步条件为资源计数大于0,即有可用资源。某个线程处理完共享资源后,需要调用ReleaseSemaphore释放资源,增加可用资源计数。当然,同其他内核对象一样,最终也得调用CloseHandle函数释放信号量内核对象占用的资源。

不同于过于简陋的只有一个茅坑的移动厕所,豪华一点的公共厕所往往不止一个坑位。将坑位看做资源,则坑位的个数即信号量机制中的资源计数。这一排坑位,在同一时间内也只能满足部分人的需求,一个人一个坑,有人用完了释放坑位,排队的人才有机会进入。

信号量的使用特点使其更适用于对Socket程序中线程的同步问题。一个典型的场景就是HTTP服务器要对同一时间内访问同一页面的用户数加以限制,这是可以为每一个用户对服务器的页面请求设置一个线程,而页面则是待保护的资源,通过使用信号量对线程的同步作用可以确保在任一时刻无论有多少用户对某一页面进行访问,只有不大于设置的最大用户数目的线程能够进行访问,而其他访问企图被挂起。只有在有用户退出对此页面的访问后,其他用户的访问请求才有可能得到响应。

迅雷的“原始地址线程数”即是设置客户端从某一原始地址下载资源的最大线程数。当然,资源所在的站点本身就会对某一客户连接数有限制,这里的“某一客户连接数”意即把文件拆开,一个线程下载一块的多线程协助下载。

当然,多线程并不是越多越好,迅雷下载肯定使用的是线程池。迅雷将下载线程数限制为5,符合线程池的经验公式,即线程池规模 = CPU数 * 2 + 1,现在机器基本都是双核或多CPU的。当用户建立5个以上的下载任务时,迅雷最多同时执行5个下载任务,超出的任务将排队等待。一旦有下载任务完成,另一个等待下载任务即启动。迅雷对于下载线程数的限制,即使用了信号量机制。

(4)      互斥内核对象(Mutex

互斥是一种用途非常广泛的内核对象。能够保证多个线程对同一共享资源的互斥访问。同临界区有些类似,只有拥有互斥对象的线程才具有访问资源的权限。由于互斥对象只有一个,因此就决定了任何情况下,此共享资源都不会被多个线程所访问。当前占据资源的线程在任务处理完后应该将占据的互斥对象交出,以便其他线程在其上的等待调用WaitForSingleObject/WaitForMultipleObjects返回。

基于互斥内核对象来保持线程同步用到的函数主要有CreateMutex、OpenMutex、ReleaseMutex,其用法在代码布局上同信号量内核对象。

// The CreateMutex function creates or opens a named or unnamed mutex object.

HANDLE CreateMutex(

LPSECURITY_ATTRIBUTES lpMutexAttributes,  // SD

BOOL bInitialOwner,                // initial owner

LPCTSTR lpName                      // object name

);

参数bInitialOwner主要用来控制互斥对象的初始状态,一般将其设为FALSE,以表明互斥对象在创建时并没有为任何线程所占有。最后一个参数即内核对象名称,以便跨进程执行OpenMutex按名访问。

当目前对资源具有访问权限的线程不再需要访问此资源而要离开时,必须通过ReleaseMutex函数来释放其拥有的互斥对象。

// The ReleaseMutex function releases ownership of the specified mutex object.

BOOL ReleaseMutex(

HANDLE hMutex   // handle to mutex

);

基于互斥内核对象的同步在代码上体现在对WaitForSingleObject/WaitForMultipleObjects函数的调用,以等待互斥内核对象的通知,其同步条件为某一时刻只有一个线程拥有互斥对象。

在互斥对象通知引起调用等待函数返回时,等待函数的返回值不在是WAIT_OBJECT_0或[WAIT_OBJECT_0, WAIT_OBJECT_0+nCount-1]之间的某值,而是将返回WAIT_ABANDONED_0或是[WAIT_ABANDONED_0, WAIT_ABANDONED_0+nCount-1] 之间的某值,以此来表明线程正在等待的互斥对象由另外一个线程所拥有,而此线程却在使用完共享资源前就已经终止。除此之外,使用互斥内核对象的方法在等待线程的可调度性上同使用其他几种内核对象的方法也有所不同,其他内核对象在没有得到通知时,受调用等待函数的作用,线程将会挂起,同时丧失可调度性,而使用互斥的方法可以在等待的同时仍具有调度性,这也正是互斥对象所能完成的非常规操作之一。

在编写程序时,互斥对象多用在对那些为多个线程所访问的内存块的保护上,可以确保任何线程在处理此内存快时,都对其拥有可靠的独占访问权。

(5)      其他(互锁函数和旋转锁)

互锁函数

互锁函数为同步访问多线程共享变量提供了一个简单的机制。如果变量在共享内存,不同进程的线程也可以使用此机制。用于互锁的函数有InterlockedIncrement、 InterlockedDecrement等。

InterlockedIncrement 函数递增(加 1)指定的 32 位变量。这个函数可以阻止其他线程同时使用此变量,函数原型如下。

// The InterlockedIncrement function increments (increases by one) the value of the specified variable and checks the resulting value. The function prevents more than one thread from using the same variable simultaneously.

LONG InterlockedIncrement(

LPLONG volatile lpAddend   // variable to increment

);

InterlockedDecrement函数同步递减(减1)指定的32位变量,原型如下。

// The InterlockedDecrement function decrements (decreases by one) the value of the specified variable and checks the resulting value. The function prevents more than one thread from using the same variable simultaneously.

LONG InterlockedDecrement(

LPLONG volatile lpAddend   // variable address

);

Interlocked系列函数以原子方式操控一个值。所谓原子操作,即在多进程(线程)的操作系统中不能被其它进程(线程)打断的操作就。它的实现,取决于代码运行的CPU平台。如果是x86系列CPU,则Interlocked函数会在总线上维持一个硬件信号,这个信号会阻止其他CPU访问同一内存地址。

InterlockedExchangeAdd函数可以替代InterlockedIncrement/InterlockedDecrement,可以加减任何值。InterlockedExchange和InterlockedExchangePointer函数可实现原子级别的赋值(访问)操作。它们在实现旋转锁时极其有用。

旋转锁

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

为了使用临界区的时候同时使用旋转锁,Windows提供了一套在临界区基础上添加SPIN计数的API,使用InitializeCriticalSectionAndSpinCount函数替代InitializeCriticalSection,使用SetCriticalSectionSpinCount进行SPIN COUNT的设置。

旋转锁假定被保护的资源只会被占用一小段时间。与切换到内核模式然后等待相比,这种优先用户模式下的旋转等待效率会更高。只要在经历了指定次数(SpinCount = 4000)后,仍然无法访问资源时,线程才会真正切换到内核模式,并一直等待到资源可供使用为止(此时,它不消耗CPU时间)。这就是临界区(CRITICAL_SECTION)的实现方式。

(6)      同步机制的比较

在同步技术中,临界区是最容易掌握的。它是一种简单的数据结构。同通过等待和释放内核态互斥对象实现同步的方式相比,临界区的速度明显胜出。但是临界区非内核对象,不能用在多进程间的线程同步,只能用于单个进程内部的线程同步。但是,对于普通的单进程程序,鉴于临界区使用旋转锁优先在用户模式同步的高效性,使用临界区不失为一种普遍的同步方案。

事件内核对象是一种基本的内核对象,被广泛使用于多进程、多线程同步通信。实际上,临界区RTL_CRITICAL_SECTION内部即使用了事件内核对象。

信号量机制在内核模式工作,适用于允许特定个数的线程执行某任务。

互斥对象与其他内核对象不同,互斥对象在操作系统中拥有特殊代码,并由操作系统来管理。

关于跨进程多线程的高速同步,参考《Windows内核编程》第四版第10章《线程同步工具包》。Jeffrey Richter将自己改良的临界区封装成一个COptex类,只有在必要时才进入核心态,尽可能在用户态同步,从而实现对同步的优化。可参考飞鸽传书程序员蔡镇定的博文《使用临界段实现优化的进程间同步对象