目前,我正在深入研究大量使用MFC类的遗留代码库。它使用CCriticalSection
作为互斥对象,使用WaitForSingleObject
来“锁定”该互斥对象。代码大致如下所示:
struct Foo {
static CCriticalSection mutex;
void doSomeWriting() {
mutex.Lock();
…
mutex.Unlock();
}
void doSomeReading() {
WaitForSingleObject(mutex, some_timeout);
…
// No unlocking here!
}
};
我会提供一个MWE,但是如果要信任我的Visual,使用MFC的最小应用程序似乎需要几千行代码。
我目前正在应用验证器下运行应用程序,该应用程序标记WaitForSingleObject()
调用,并抱怨mutex
中的句柄为NULL。
我知道我从代码中获得了应该驱逐CCriticalSection吗? (事实上,我们正在对其进行重构以使用std::recursive_mutex
),但我至少想知道最初的作者想要用它实现什么。
不幸的是,我找不到关于WaitForSingleObject
和CCriticalSection
如何交互的文档。CCriticalSection文档根本没有提到WaitForSingleObject
,WaitForSingleObject文档也没有提到CCriticalSection
。使用关键部分的示例只处理CRITICAL_SECTION
对象,这些对象显然需要进行初始化。此外,在这些示例中没有使用WaitForSingleObject
。
我的问题
WaitForSingleObject
上调用CCriticalSection
?CCriticalSection
上的WaitForSingleObject
之前,是否需要以某种方式初始化它?“应用程序验证程序”错误似乎表明了这一点。WaitForSingleObject
锁定CCriticalSection
了吗?或者它只是阻塞,直到关键部分被解锁?doSomeReading()
方法不需要再次解锁关键部分吗?doSomeWriting()
不会在线程A中启动,而线程B正在忙着执行doSomeReading()
(在WaitForSingleObject()
调用之后),对吗?发布于 2021-04-21 17:26:32
应用验证器..。标记
WaitForSingleObject()
调用,并抱怨互斥体中的句柄为NULL
。
应用验证器是正确的。这就是正在发生的事。这也很明显,为什么会发生这种情况。这就不那么明显了,为什么微软认为强制CCriticalSection进入CSyncObject类层次结构是个好主意。
让我们从基类CSyncObject
开始,它看起来像这样(与问题无关的所有内容都被去掉了):
struct CSyncObject {
CSyncObject() : m_hObject{nullptr} {}
operator HANDLE() const { return m_hObject; }
HANDLE m_hObject;
}
有一个默认的c‘’tor来初始化唯一的数据成员(m_hObject
),还有一个转换操作符(operator HANDLE()
),这样CSyncObject
类型的对象就可以传递给需要HANDLE
类型的参数的函数(比如WaitForSingleObject
)。
除了数据成员是公共的以外,这实际上只是一个非常标准的包装器,它包含一个以HANDLE
表示的本地Windows同步原语。当我们看一看CCriticalSection
时,事情就开始转向一边
struct CCriticalSection : CSyncObject { // <- Look ma, I'm a CSyncObject!
CCriticalSection() { /* Initialize m_sect */ }
operator CRITICAL_SECTION*() { return &m_sect; }
CRITICAL_SECTION m_sect;
}
再说一次,作为一个公共数据成员,无论如何都要欣赏它的一致性。但是看,还有更多!CCriticalSection
是从CSyncObject
继承的,所以现在它也有了m_hObject
。正如您可能已经猜到的,除了基类‘c’to将其初始化为(应用程序验证程序告诉您的NULL
)之外,它从不触及、读取或更改其他任何内容。
如果CCriticalSection
没有继承公共operator HANDLE()
,这就不会那么糟糕了。有了它,CCriticalSection
现在可以在任何需要HANDLE
的地方使用。比如,比如说,WaitForSingleObject
。
有了所有这些,WaitForSingleObject(mutex, some_timeout)
编译,甚至运行,没有立即失败。当然,它失败了参数验证,但是当您不检查返回值时.说到底,这只是一个非常冗长的不操作。当然,它不会等待任何东西,也不会防止并发执行。它需要修理。
其余问题如下:
是否允许在
WaitForSingleObject
上调用CCriticalSection
?
当然,就像我们看到的。CCriticalSection
很难伪装成一个CSyncObject
,所以编译器不会介意。
在调用
CCriticalSection
上的WaitForSingleObject
之前,是否需要以某种方式初始化它?“应用程序验证程序”错误似乎表明了这一点。
是。关键部分需要使用InitializeCriticalSection或InitializeCriticalSectionAndSpinCount初始化。这并不能使CRITICAL_SECTION
成为WaitForSingleObject
的合法论据。
WaitForSingleObject
锁定CCriticalSection
了吗?或者它只是阻塞,直到关键部分被解锁?
不,但EnterCriticalSection将获得所有权,这是通过调用LeaveCriticalSection发布的。
如果它锁定:在我的示例中,
doSomeReading()
方法不需要再次解锁关键部分吗?
如果它使用正确的API调用来获得所有权,它就会这样做。因为它什么也不做,所以也没有必要发布任何东西。
如果它不执行锁定:那么上面的示例并不保证
doSomeWriting()
不会在线程A中启动,而线程B正在忙着执行doSomeReading()
(在WaitForSingleObject()
调用之后),对吗?
对,是这样。线程A和B可以并发地输入doSomeWriting()
。没有实现任何同步。
发布于 2021-04-21 20:10:14
其他成员提交的答案很好地回答了你的问题。总结如下:
WaitForSingleObject()
对象调用CCriticalSection
是错误的,因为关键部分不是一个可延迟的对象,尽管在CCriticalSection
中它是从CSyncObject
继承的。关键部分为相同进程的线程提供了对代码部分的独占/序列化访问(就像Mutex对象一样,但有上述限制--而且它们也更有效)。具有关键部分的唯一有效操作是“输入/离开”(锁定/解锁)。CCriticalSection
类构造函数(它只是它的包装器)为您完成了这一任务。WaitForSingleObject()
不会锁定关键部分,锁定关键部分的唯一方法是调用EnterCriticalSection()
(Win32)或CCriticalSection::Lock()
(MFC) --在这两种情况下都不会超时。WaitForSingleObject()
调用并不真正等待或锁定任何东西(这里的使用是错误的),是的,在线程B忙于执行doSomeReading()
时,可以在线程A中输入doSomeWriting()
中的关键部分。但是没有两个线程可以并发地进入doSomeWriting()
中的关键部分。我认为作者试图通过以下方式实现对共享资源的同步访问:
后者应该是通过WaitForSingleObject()
呼吁来实现的,但这显然是一种误解。
正如您提供的链接中所指出的,MFC的CSyncObject
-derived对象有它们自己的问题,因此您需要另一种实现。
您可以使用Win32同步对象。临界截面对象实际上是可重入的(尽管我认为这一点都没有用)。它们也可以与条件变量结合使用。您可以使用这样的实现来防止作者在读取器工作时更新共享资源,以及一个(手动重置) 事件对象(由作者设置/清除),以防止读者在写入时访问资源( WaitForSingleObject()
调用应该做的事情)。
然而,尽管我承认我从未真正尝试过这种机制,但我认为最适合(我敢确切地说)您想要实现的是Slim阅读器/作家(SRW)锁。它们为读写提供同步,通过使用单个对象实现“单作者-多读取器”功能。您可以将这些调用封装在一个C++类中,但是我认为这一点没有什么好处,因为Win32实现已经非常“高级”了,调用将是1:1。
发布于 2021-04-21 18:14:29
不是真正的答案,但作为对你的回应
使用MFC的最小应用程序似乎是几千行代码。
下面是:
#include <afxmt.h>
#include <thread>
#include <chrono>
struct Foo {
static CCriticalSection mutex;
void doSomeWriting() {
mutex.Lock();
std::this_thread::sleep_for(std::chrono::seconds(5));
mutex.Unlock();
}
void doSomeReading() {
WaitForSingleObject(mutex, 1000);
//…
// No unlocking here!
}
};
CCriticalSection Foo::mutex;
int main()
{
Foo f;
std::thread t = std::thread(&Foo::doSomeWriting, f);
f.doSomeReading();
t.join();
}
https://stackoverflow.com/questions/67194360
复制相似问题