MFC WaitForSingleObject 和 CCriticalSection - 如何使用?

MFC WaitForSingleObject and CCriticalSection - how to use?

我目前正在研究大量使用 MFC 类 的遗留代码库。它使用 CCriticalSection 作为互斥量,使用 WaitForSingleObject 到 'lock' 该互斥量。代码大致如下所示:

struct Foo {
  static CCriticalSection mutex;

  void doSomeWriting() {
     mutex.Lock();
     …
     mutex.Unlock();
  }

  void doSomeReading() {
     WaitForSingleObject(mutex, some_timeout);
     …
     // No unlocking here!
  }
};

我会提供一个 MWE,但是如果我的 Visual Studio 可信的话,使用 MFC 的最小应用程序似乎有几千行代码。

我目前 运行 Application Verifier 下的应用程序,它标记 WaitForSingleObject() 调用并抱怨 mutex 中的句柄为 NULL。

我从代码中知道我 should just banish CCriticalSection(事实上我们正在重构它以使用 std::recursive_mutex),但我想至少知道是什么原作者想以此来实现

遗憾的是,我找不到关于 WaitForSingleObjectCCriticalSection 如何交互的文档。 CCriticalSection docs don't mention WaitForSingleObject at all, and the WaitForSingleObject docs do not mention CCriticalSection. The example for using critical sections 仅处理 CRITICAL_SECTION 对象,这些对象显然需要以某种方式进行初始化。此外,这些示例中未使用 WaitForSingleObject

我的问题

Application Verifier [...] flags the WaitForSingleObject() call and complains about a handle inside mutex being NULL.

应用程序验证器是正确的。这正是正在发生的事情。这也很明显,为什么会这样。远没有那么明显,为什么微软决定强制 CCriticalSection into the CSyncObject class 层次结构是个好主意。

让我们从基础 class 开始,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)。

除了数据成员 public 之外,这实际上只是一个非常标准的包装器,它围绕着表示为 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;
}

再次强调,public 数据成员无论如何都要欣赏一致性。但是,看,还有更多! CCriticalSection 继承自 CSyncObject,所以现在它也有一个 m_hObject。其中,正如您可能已经猜到的那样,它从不接触、读取或更改除基础 class 之外的任何内容,或者将其初始化为(应用程序验证程序告诉您的 NULL) .

如果 CCriticalSection 没有继承 public operator HANDLE(),那 还不至于 都不好。这样一来,现在可以在任何需要 HANDLE 的地方使用 CCriticalSection。比如说,WaitForSingleObject.

有了所有这些,WaitForSingleObject(mutex, some_timeout) 可以编译,甚至可以运行而不会立即失败。当然,它没有通过参数验证,但是当您不检查 return 值时……它归结为只是一个非常冗长的空操作。它当然不会等待任何事情,也不会防止并发执行。它需要修复。

剩余问题:

Is calling WaitForSingleObject on a CCriticalSection even allowed?

当然,正如我们所见。 CCriticalSection 努力伪装成 CSyncObject 这样编译器就不会介意了。

Is it necessary to somehow initialize a CCriticalSection before calling WaitForSingleObject on it? The Application Verifier error seems to indicate this.

是的。关键部分需要用 InitializeCriticalSection or InitializeCriticalSectionAndSpinCount 初始化。这不会使 CRITICAL_SECTION 成为 WaitForSingleObject.

的合法论据

Does WaitForSingleObject lock the CCriticalSection? Or does it just block until the critical section is unlocked?

没有,但是 EnterCriticalSection will take ownership, that's released with a call to LeaveCriticalSection.

If it locks: Wouldn't the doSomeReading() method in my example need to unlock the critical section again?

如果它使用正确的 API 调用来取得所有权,它会的。因为它什么都不做,所以也不需要释放任何东西。

If it does not do locking: Then the example above does not guarantee that doSomeWriting() isn't started in thread A while thread B is busy executing doSomeReading() (after the WaitForSingleObject() call), right?

正确。线程A和B可以并发进入doSomeWriting()。没有实现任何同步。

不是真正的答案,而是回应你的

minimal application using MFC seems to be several thousand lines of code

这里是:

#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();
}

其他成员提交的答案很好地涵盖了您的问题。总结:

  • CCriticalSection 对象调用 WaitForSingleObject() 是错误的,因为临界区不是可等待的对象,尽管在 MFC 中它继承自 CSyncObject。关键部分为同一进程的线程提供 exclusive/serialized 对一段代码的访问(就像 Mutex 对象,但有上述限制 - 而且它们也更有效)。临界区的唯一有效操作是 Enter/Leave (Lock/Unlock).
  • Win32 关键部分需要初始化,但是 CCriticalSection class 构造函数(只是它的包装器)会为您完成此操作。
  • WaitForSingleObject() 不会锁定临界区,锁定临界区的唯一方法是调用 EnterCriticalSection() (Win32) 或 CCriticalSection::Lock() (MFC) - 无超时在这两种情况下。
  • 因为 WaitForSingleObject() 调用并没有真正等待或锁定任何东西(这里只是错误使用),是的,doSomeWriting() 中的临界区可以在线程 A 中进入,而线程 B 是忙于执行 doSomeReading()。但是在doSomeWriting()中没有两个线程可以同时进入临界区。

我认为作者试图实现的是通过以下方式同步访问共享资源:

  • 一次只有一位作家。
  • 多个并发读取器,同时没有写入。

后者本应通过 WaitForSingleObject() 调用实现,但这显然是一种误解。

正如您在提供的链接中指出的那样,MFC 的 CSyncObject 派生对象有其自身的问题,因此您需要一个替代实现。

您可以使用 Win32 同步对象。 Critical Sections objects are actually re-entrant (although I don't think this is useful at all). They can also be used in combination with Condition Variables. You could use such an implementation to prevent writers updating the shared resource while readers work, along with a (Manual-reset) Event 对象(作者set/cleared),以防止读者在写入时访问资源(WaitForSingleObject() 调用应该做的)。

然而,尽管我承认我从未实际尝试过这种机制,但我认为最适合(我敢说准确地说)你想要实现的是 Slim Reader/Writer (SRW) Locks。它们为读取和写入提供同步,通过使用单个对象实现“单个写入器 - 多个读取器”功能。您可以将这些调用封装在 C++ class 中,但我看不出这样做有什么好处,因为 Win32 实现已经相当“高级”,调用将是 1:1。查看文档。