一个快速的、大部分用户模式的进程间互斥体






3.31/5 (25投票s)
2002年11月30日
9分钟阅读

187717

1129
Win32 互斥体既慢又笨拙,这是一种快速而巧妙的替代方案!
引言
我最近在一个项目中工作,其中有多个资源需要由多个进程共享。很简单!我使用了 Win32 互斥体。起初看起来还可以,但在运行各种进程可以同时锁定和释放互斥体数十万次的测试场景时,性能成了一个问题。我将性能不佳归因于 Win32 互斥体(原因后面会讲),因此决定编写自己的互斥体类。本文介绍了我创建的互斥体类(实际上是一个 DLL,这是必需的,但稍后会讲到)、它的工作原理以及它与 Win32 互斥体的对比。
Win32 互斥体的问题
Win32 互斥体以速度慢而闻名。每次创建互斥体时,操作系统都必须从“用户模式”切换到“内核模式”,这本身可能就需要数百条 CPU 指令。如果您只是偶尔锁定一个共享资源,那倒没什么大不了的。在这种情况下(尤其是在涉及网络连接时),您的大部分代码可能已经花费了更多时间。但是,如果您像我上面专有数据库文件示例那样,持续不断地将资源作为主要信息来源,那么 Win32 互斥体所做的所有额外工作都可能成为一个真正的性能瓶颈。
解决方案
我通过编写自己的互斥体解决了这个问题。它结合使用了进程间共享内存、Win32 临界段(仅在发生争用时才切换到“内核模式”)以及 Windows 的 InterlockedExchange
函数。我使用的进程间共享内存是一种方法:在二进制文件(可执行文件或 DLL)中创建一个数据段,然后将其标记为共享内存。在此数据段中,我共享的唯一内存是两个小变量(一个 LONG
和一个 DWORD
)。我排除了使用内存映射文件和其他形式的进程间通信,因为我不想因为使用这些方法而 incurring 性能损失。选择共享内存数据段选项是有代价的:必须创建一个 DLL 来托管我们的互斥体(如果您不介意,也可以将其合并到现有的 DLL 中)。创建 DLL 项目后,需要在项目中的一个文件(最好是 *.cpp 文件)中添加如下所示的语句。
#pragma data_seg(".USRMUTEX_SHARED")
volatile DWORD g_dwDSMutexOwner = 0;
volatile LONG g_lLocker = 0;
#pragma data_seg()
#pragma comment(linker, "/section:.USRMUTEX_SHARED,rws")
互斥体对象将实现为一个 C++ 类,我称之为“互斥体类”。该类及其方法将作为 DLL 的导出。互斥体类本质上只有构造函数和析构函数作为其方法,以及三个成员变量,所有这三个变量都是静态的。第一个静态成员变量是一个 unsigned long
,其目的是跟踪同一进程中的同一线程获得互斥体的独占访问权限的重复次数(“独占访问”的另一种说法是“锁定”;我说的“重复”是指“在释放所有锁之前锁定第二次、第三次或更多次”)。第二个静态成员变量保存了创建我们正在操作的互斥体类实例的进程的 ID。第三个静态成员变量是一个 Win32 临界段。我们将前两个静态成员变量初始化为零,并将第三个变量使用 InitializeCriticalSection
进行初始化。
现在我们已经处理了静态成员变量,让我们来模拟类原型。请记住,我们只有一个默认构造函数和析构函数。在开始实现之前,最好将类的默认复制构造函数和默认复制运算符设为不可达(将这些方法的原型放在类的“private”部分,但实际上不要实现它们)。现在我们有了类原型,让我们来实现方法。我们在构造函数中所做的第一件事是询问当前进程的进程 ID 是否已设置到我们上面提到的第二个静态成员变量中。如果第二个静态成员变量仍设置为 0
,则表示我们还没有设置,因此使用 GetCurrentProcessId
获取当前进程 ID,并将其设置到此变量中。然后使用 EnterCriticalSection
锁定我们的临界段静态成员变量。通过这样做,我们完成了两件事:
- 我们确保当前进程中的当前线程将是获得互斥体独占访问权的唯一可用线程。
- 通过进入临界段,我们已经为获得互斥体的独占访问权完成了一半。
- 哪个进程拥有它;以及
- 该进程中的哪个线程拥有它。
g_lLocker
)。使用该变量,我们创建一个循环,直到对该变量(&g_lLocker
)和 1
调用 InterlockedExchange
的结果为 0
时,该循环才会中断。以下是如何实现代码的示例: while (::InterlockedExchange(&g_lLocker, 1) != 0)
{
Sleep(0);
}
这表示:“如果我将该变量设置为 1
,但在此之前该变量的值是 0
,那么共享内存对我来说是安全的。通过将其设置为 1
,我告诉其他人需要“旋转”(循环直到条件为真)直到我完成它。”
其中的诀窍在于,我们必须确保在完成时将该共享内存变量设置回 0
,以便旋转的线程能够获得独占使用权。因此,完成后,我们使用 InterlockedExchange
函数将其设置回 0
,如下所示:
::InterlockedExchange(&g_lLocker, 0);
一旦我们获得了对共享内存的独占访问权,我们就将另一个共享内存变量(g_dwDSMutexOwner
)与 0
和保存我们进程 ID 的静态内存变量进行比较。如果该共享 DWORD
等于这两个值中的任何一个,我们就将当前进程 ID 从该静态成员变量复制到我们的共享 DWORD
中。无论比较是否通过,我们都需要将 g_lLocker
变量设置回 0
(使用上面描述的 InterlockedExchange
)以给其他等待的线程一个比较的机会。一旦比较通过并发生复制,互斥体将被认为已被调用线程的该调用进程“锁定”。此时,我们所要做的就是递增跟踪同一进程中的同一线程锁定互斥体次数的静态成员变量。如果上面的初始比较失败(共享 DWORD
不是 0
或当前进程 ID),那么我们在这里调用 Sleep(1)
(即使使用 0
会放弃当前线程的剩余时间片,但我发现使用非 0
的值效果更好)。接下来,我们通过循环回到使用 g_lLocker
变量获得共享内存独占访问权的点来再次测试比较,但不要回到进入临界段的地方(此时我们还没有离开临界段)。让这个循环一直持续下去,直到我们得到一个通过的比较。比较失败的事实意味着同一进程中或另一个进程中的某个其他线程已经拥有了互斥体。
析构函数实际上要简单得多。首先,我们递减跟踪同一进程中的同一线程锁定互斥体次数的静态成员变量。然后,我们检查该变量是否为零;如果是,则需要使用上面在构造函数中描述的方法,使用(g_lLocker
)获得对共享内存变量的独占访问权。一旦我们获得独占访问权,就将我们的共享 DWORD
(g_dwDSMutexOwner
)设置为 0
。这完全解锁了当前进程对互斥体的锁定,并允许其他线程获得访问权。现在,释放我们对共享内存的独占访问权(使用上面描述的 InterlockedExchange
将 g_lLocker
设置为 0
)。最后,无论我们的第一个静态成员变量现在是否为零,都离开我们在构造函数中进入(并一直保持)的临界段。最后一步允许当前进程中的其他线程尝试获得互斥体的独占访问权。
CUsrMutex 类
这是我们刚刚创建的类的类定义(我称之为 CUsrMutex
)。完整的类实现可以在本文附带的 zip 文件中找到。
//This is a class we made for CUsrMutex to consume, it makes the win32
// critical sections easier to use
class CCritSec
{
protected:
CCritSec(const CCritSec& src);
const CCritSec& operator=(const CCritSec& src);
public:
CCritSec();
virtual ~CCritSec();
operator LPCRITICAL_SECTION();
bool Lock();
bool Unlock();
protected:
mutable CRITICAL_SECTION m_sect;
};
//This class makes for easy locking of our g_lLocker shared memory variable
class CUsrMutexLockerWkr
{
private:
CUsrMutexLockerWkr(const CUsrMutexLockerWkr& src);
const CUsrMutexLockerWkr& operator=(const CUsrMutexLockerWkr& src);
public:
CUsrMutexLockerWkr();
~CUsrMutexLockerWkr();
};
class CUsrMutex
{
protected:
CUsrMutex(const CUsrMutex& src);
const CUsrMutex& operator=(const CUsrMutex& src);
public:
CUsrMutex();
~CUsrMutex();
protected:
static unsigned long m_ulLockCnt;
static DWORD m_dwProcessId;
static CCritSec m_dscs;
};
这里是这三个类的实现。
#pragma data_seg(".USRMUTEX_SHARED")
volatile DWORD g_dwDSMutexOwner = 0;
volatile LONG g_lLocker = 0;
#pragma data_seg()
#pragma comment(linker, "/section:.USRMUTEX_SHARED,rws")
unsigned long CUsrMutex::m_ulLockCnt(0);
DWORD CUsrMutex::m_dwProcessId(0);
CCritSec CUsrMutex::m_dscs;
CCritSec::CCritSec() :
m_sect()
{
::InitializeCriticalSection(&m_sect);
}
CCritSec::~CCritSec()
{
::DeleteCriticalSection(&m_sect);
}
CCritSec::operator LPCRITICAL_SECTION()
{
return (LPCRITICAL_SECTION)&m_sect;
}
bool CCritSec::Lock()
{
::EnterCriticalSection(&m_sect);
return TRUE;
}
bool CCritSec::Unlock()
{
::LeaveCriticalSection(&m_sect);
return TRUE;
}
CUsrMutexLockerWkr::CUsrMutexLockerWkr()
{
while (::InterlockedExchange(&g_lLocker, 1) != 0)
{
Sleep(0);
}
}
CUsrMutexLockerWkr::~CUsrMutexLockerWkr()
{
::InterlockedExchange(&g_lLocker, 0);
}
CUsrMutex::CUsrMutex()
{
if (m_dwProcessId == 0)
{
assert(sizeof(LONG) == sizeof(DWORD));
m_dwProcessId = ::GetCurrentProcessId();
assert(m_dwProcessId != 0);
}
m_dscs.Lock();
for (;;)
{
{
CUsrMutexLockerWkr locker;
if ((g_dwDSMutexOwner == 0) ||
(g_dwDSMutexOwner == m_dwProcessId))
{
g_dwDSMutexOwner = m_dwProcessId;
break;
}
}
Sleep(1);
}
++m_ulLockCnt;
}
CUsrMutex::~CUsrMutex()
{
if (--m_ulLockCnt == 0)
{
CUsrMutexLockerWkr locker;
assert(g_dwDSMutexOwner == m_dwProcessId);
g_dwDSMutexOwner = 0;
}
m_dscs.Unlock();
}
证明性能
在我实现了上述类之后,我想对其进行测试,以查看它是否真的比 win32 互斥体更快。我进行了大量测试,我的类始终占优势。在 6 个独立进程争夺互斥体的测试中,它表现尤为出色。这 6 个进程首先尝试锁定和解锁 Win32 互斥体 100,000 次,然后尝试锁定和解锁我的互斥体 100,000 次。每次的总体时间都很相似,大致如下:
Win32 互斥体:锁定和解锁互斥体花费了 6800 毫秒
我的互斥体:锁定和解锁互斥体花费了 240 毫秒
随本文附带的 zip 压缩包包含了我对互斥体的实现、一个示例应用程序以及几个性能和比较测试应用程序。
限制
此解决方案有两个主要限制:1) 它必须驻留在 DLL 中(我想您也可以将其直接放入可执行文件中,如果这符合您的特定需求),2) 您只能共享一个互斥体。第二个限制仅是我的特定实现的一个限制。您可以在您的实现中进行更改,以便访问多个这样的互斥体。
结论
该解决方案满足了我的需求,并极大地提高了我们产品的性能。尽管此自定义互斥体类的实现可能不符合您的需求,但我认为(并希望)这已经成为一个很好的资源,可以帮助您开始构建自己的自定义同步对象。祝您好运!
历史
11/23/2002 | - | 初始版本。 |
02/18/2003 | - | 通过添加 g_lLocker 修复了竞态条件的可能性。 |