通过 Win32 同步对象确保单例执行
同步对象句柄:实现单例运行的另一种机制。
引言
有很多场景需要单例运行,这意味着强制只有一个程序实例在运行。传统上,有几种技术可以防止加载第二个实例。列举一些:
- 使用全局静态标志
- 检查窗口标题
- 创建文件锁
本文将探讨 Windows 平台上的另一种机制,使用同步对象,即事件、互斥体和信号量,来保证程序的单例运行。
背景
虽然互斥体/信号量/事件最初是为解决多线程程序中的竞态条件和死锁问题而构思的,但它们在 Windows 上的创建具有一个固有的属性:任何命名对象在相同条件下只能在全局命名空间中创建一次。这个全局唯一的句柄(尽管 Win32 提供了复制句柄的 API)相当于一个建议性锁。当所有观察方都遵守该锁时,它基本上可以作为第一个运行实例的明确衡量标准。
Using the Code
互斥体、信号量和事件分别有三对 CPP 和头文件。将其中一对与基类一起包含到您的项目中,您将获得程序单例运行的效果。
附带的测试套件提供了如何使用这些实用类的一个示例。
CSingleton *pSingleton = NULL;
switch( type )
{
case eMutex:
pSingleton = new CMutexSingleton(MUTEX_NAME, NULL);
if( pSingleton )
{
if( pSingleton->IsExclusiveRun() )
{
bWait = true;
printf("Running (mutex singleton)");
}
else
{
printf("another instance (mutex) is running, let's go home\n");
}
}
break;
关注点
深入讨论同步需要大量的文字。本文侧重于一个方面:同步对象的创建,然后将对象句柄引向我们感兴趣的领域:单例运行。一路上,我将尝试用通俗易懂的语言回答一些让非英语或非 C++ 母语者感到困惑的相关问题(作者承认自己也是其中之一:-))
- 什么是信号?互斥体的信号与信号量或事件的信号相同吗?
- 互斥体发出信号后会发生什么?
- 同理,这些对象的取消信号又如何?
- 我可以使用临界段来实现与互斥体/信号量/事件相同的结果吗?
在多个在线或印刷来源中,我认为 Julian Templeman 对信号和取消信号的解释最好。在他的书《Windows NT 编程入门》第 222 页,有一个表格列出了各种对象类型以及“信号”的相应含义。
已信号的互斥体意味着它不被任何进程拥有。换句话说,已信号的互斥体是可以被获取的。违反直觉?这就是它的结构方式。当信号量发出信号时,意味着它的计数大于零。普遍的说法是信号量是 n 路互斥体。换句话说,互斥体是信号量的一个特例,其计数为 1。Julian 指出了计数为 1 的信号量与互斥体之间的一个细微差别;那就是,“在 Win32 中,计数为 1 的信号量没有 WAIT_ABANDONED
返回值”。当有人稍后在计数为 1 的信号量上调用 WaitForXXXX
时,这种晦涩的行为很重要。不要指望在该信号量句柄上获得 WAIT_ABANDONED
。
在这三种同步对象中,事件信号/取消信号的操作最为棘手。
通过标准的 Win32 CreateEvent
创建事件后,需要调用以下函数之一:
SetEvent()
ResetEvent()
PulseEvent()
由于事件可以手动或自动创建,并且其初始状态可以是已信号或未信号,让我们检查上述哪个调用用于正确设置事件。
对于自动事件:SetEvent()
和 PulseEvent
具有相同的效果,它们将事件设置为已信号,然后返回到未信号状态,唤醒单个等待线程(《Windows NT 编程入门》第 251 页)。
对于手动事件:SetEvent()
将事件设置为已信号并保持在那里;ResetEvent()
将事件设置为未信号并保持在那里;PulseEvent()
将事件设置为已信号,然后返回到未信号状态,唤醒所有等待线程(来源:同上)。
总之,如果您想创建一个事件并一直持有它,请创建一个手动事件并对其句柄调用 ResetEvent()
。
取消信号具有相反的效果:取消互斥体的信号意味着它不再可用,而取消信号量的信号表示其计数已返回到 0。最后,取消事件的信号意味着它已从其先前状态中取消设置。
临界段是另一种广泛使用的同步机制。与上述三种不同,它仅仅是一个结构,而不是一个内核对象,在创建过程中可见。本文最终的目标是在每个机器的基础上实现单例运行。因此,临界段不适合在机器范围的全局命名空间中完成此任务。
历史
- 首次提交:2008 年 8 月 11 日。