【问题标题】:ReadDirectoryChangesW and GetOverlappedResultReadDirectoryChangesW 和 GetOverlappedResult
【发布时间】:2017-04-27 18:04:35
【问题描述】:

我正在异步调用ReadDirectoryChangesW 以监视后台线程中的目录更改。

这是打开目录 (basePath) 并启动“阅读”线程的方式:

    m_hDIR = CreateFileW(
            basePath,
            FILE_LIST_DIRECTORY | GENERIC_READ,
            FILE_SHARE_WRITE | FILE_SHARE_READ,
            NULL,
            OPEN_EXISTING,
            FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
            NULL);

        if (m_hDIR == INVALID_HANDLE_VALUE)
            throw CrException(CrWin32ErrorString());

        //Start reading changes in background thread
        m_Callback = std::move(a_Callback);
        m_Reading = true;
        m_ReadThread = std::thread(&CrDirectoryWatcher::StartRead, this);

这是StartRead():(注意:m_Readingatomic<bool>

void StartRead()
        {
            DWORD dwBytes = 0;
            FILE_NOTIFY_INFORMATION fni{0};
            OVERLAPPED o{0};

            //Be sure to set the hEvent member of the OVERLAPPED structure to a unique event.
            o.hEvent = CreateEvent(0, 0, 0, 0);

            while(m_Reading)
            {
                if (!ReadDirectoryChangesW(m_hDIR,
                    &fni, sizeof(fni),
                    TRUE, FILE_NOTIFY_CHANGE_LAST_WRITE,
                    &dwBytes, &o, NULL))
                {
                    CrAssert(0, CrWin32ErrorString());
                }

                if (!GetOverlappedResult(m_hDIR, &o, &dwBytes, FALSE))
                    CrAssert(0, CrWin32ErrorString());

                if (fni.Action != 0)
                {
                    std::wstring fileName(fni.FileName, fni.FileNameLength);
                    m_Callback(fileName);
                    fni.Action = 0;
                }
            }
        }

基本上,我在每一帧都“轮询”新的变化。 现在,当我调用 GetOverlappedResult() 时,它会失败并产生以下错误:

重叠的 I/O 事件未处于信号状态。

我错过了什么吗? ReadDirectoryChangesW 是否意味着每个“滴答”都被称为?或者只是在检测到新的变化时?

注意:当我省略 OVERLAPPED 结构(和 GetOverlappedResult)时,它可以工作,但会阻塞线程,直到读取更改。这会阻止我的应用程序正确终止。 (即我无法加入线程)

【问题讨论】:

  • 您尝试过if (!GetOverlappedResult(m_hDIR, &o, &dwBytes, TRUE)) - 如果我正确阅读了GetOverlappedResult function doc,那应该会阻塞您的线程,直到真正有东西要阅读。
  • 在操作完成之前您无法获得结果。这允许您做其他事情,但基本问题是您没有做其他事情。代码坚持立即得到结果。所以使用重叠 I/O 没有任何意义。查看 RegisterWaitForSingleObject() 以(可能)使其更像您的意图。或者使用工作线程。
  • 真的是同步调用 ReadDirectoryChangesW。在 OVERLAPPED 中使用事件最糟糕的方式 - 比较 IOCP 和 apc
  • @PhilBrubaker 但这会导致线程“无法连接”,对吧?
  • @HansPassant RegisterWaitForSingleObject() 是否与 OVERLAPPED 一起使用?因为没有提供结构 ReadDirectoryChangesW 块。

标签: c++ windows multithreading winapi


【解决方案1】:

在调用GetOverlappedResult()时,如果将bWait参数设置为FALSE,并且I/O操作还没有完成,GetOverlappedResult()会失败并返回ERROR_IO_INCOMPLETE错误代码:

b等待[输入]
如果此参数为TRUE,并且lpOverlapped 结构的Internal 成员为STATUS_PENDING,则该函数在操作完成之前不会返回。 如果此参数为FALSE,且操作仍处于挂起状态,则函数返回FALSEGetLastError 函数返回ERROR_IO_INCOMPLETE

这不是致命错误,因此请忽略该错误并继续。

是的,请确保在 GetOverlappedResult() 报告之前的 I/O 操作已首先完成之前,您不会再次调用 ReadDirectoryChangesW()

现在,话虽如此,您的代码还有另一个问题。您的线程正在堆栈上分配一个 FILE_NOTIFY_INFORMATION 实例。如果你看FILE_NOTIFY_INFORMATION的定义,它的FileName字段是变长的:

typedef struct _FILE_NOTIFY_INFORMATION {
  DWORD NextEntryOffset;
  DWORD Action;
  DWORD FileNameLength;
  WCHAR FileName[1];
} FILE_NOTIFY_INFORMATION, *PFILE_NOTIFY_INFORMATION;

文件名
variable-length 字段,包含相对于目录句柄的文件名。文件名采用 Unicode 字符格式,并且不是以空值结尾的。

这意味着静态分配 FILE_NOTIFY_INFORMATION 将太小,而 dwBytes 几乎总是为 0,因为 ReadDirectoryChangesW() 将无法返回完整的 FILE_NOTIFY_INFORMATION 给您(除非 @987654343 @ 长度正好是 1 个字符):

当您第一次调用ReadDirectoryChangesW 时,系统会分配一个缓冲区来存储更改信息。这个缓冲区与目录句柄相关联,直到它被关闭并且它的大小在其生命周期内不会改变。调用此函数之间发生的目录更改将添加到缓冲区中,然后在下一次调用中返回。 如果缓冲区溢出,则丢弃缓冲区的全部内容,lpBytesReturned 参数包含零,ReadDirectoryChangesW 函数失败,错误代码为ERROR_NOTIFY_ENUM_DIR

ERROR_NOTIFY_ENUM_DIR
1022 (0x3FE)
通知更改请求正在完成,并且信息未在调用者的缓冲区中返回。 调用者现在需要枚举文件以查找更改

因此,您需要动态分配一个大字节缓冲区来接收FILE_NOTIFY_INFORMATION 数据,然后您可以在GetOverlappedResult() 报告数据可用时遍历该缓冲区。

您的线程应该看起来更像这样:

void StartRead()
{
    DWORD dwBytes = 0;
    std::vector<BYTE> buffer(1024*64);
    OVERLAPPED o{0};
    bool bPending = false;

    //Be sure to set the hEvent member of the OVERLAPPED structure to a unique event.
    o.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    if (!o.hEvent) {
        CrAssert(0, CrWin32ErrorString());
    }

    while (m_Reading)
    {
        bPending = ReadDirectoryChangesW(m_hDIR,
            &buffer[0], buffer.size(),
            TRUE, FILE_NOTIFY_CHANGE_LAST_WRITE,
            &dwBytes, &o, NULL);
        if (!bPending)
        {
            CrAssert(0, CrWin32ErrorString());
        }

        while (m_Reading)
        {
            if (GetOverlappedResult(m_hDIR, &o, &dwBytes, FALSE))
            {
                bPending = false;

                if (dwBytes != 0)
                {
                    FILE_NOTIFY_INFORMATION *fni = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(&buffer[0]);
                    do
                    {
                        if (fni->Action != 0)
                        {
                            std::wstring fileName(fni->FileName, fni->FileNameLength);
                            m_Callback(fileName);
                        }

                        if (fni->NextEntryOffset == 0)
                            break;

                        fni = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(reinterpret_cast<BYTE*>(fni) + fni->NextEntryOffset);
                    }
                    while (true);
                }

                break;
            }

            if (GetLastError() != ERROR_IO_INCOMPLETE) {
                CrAssert(0, CrWin32ErrorString());
            }

            Sleep(10);
        }

        if (bPending)
        {
            CancelIo(m_hDIR);
            GetOverlappedResult(m_hDIR, &o, &dwBytes, TRUE);
        }
    }

    CloseHandle(o.hEvent);
}

在不定期轮询 I/O 状态的情况下实现此目的的另一种方法是摆脱 m_Reading 并改用可等待事件。让操作系统在它应该调用GetOverlappedResult() 或终止时向线程发出信号,这样它就可以在不忙于做某事的其余时间休眠:

m_hDIR = CreateFileW(
            basePath,
            FILE_LIST_DIRECTORY | GENERIC_READ,
            FILE_SHARE_WRITE | FILE_SHARE_READ,
            NULL,
            OPEN_EXISTING,
            FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
            NULL);

if (m_hDIR == INVALID_HANDLE_VALUE)
    throw CrException(CrWin32ErrorString());

m_TermEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (!m_TermEvent)
    throw CrException(CrWin32ErrorString());

//Start reading changes in background thread
m_Callback = std::move(a_Callback);
m_ReadThread = std::thread(&CrDirectoryWatcher::StartRead, this);

...

SetEvent(m_TermEvent);
m_ReadThread.join();

void StartRead()
{
    DWORD dwBytes = 0;
    std::vector<BYTE> buffer(1024*64);
    OVERLAPPED o{0};
    bool bPending = false, bKeepRunning = true;

    //Be sure to set the hEvent member of the OVERLAPPED structure to a unique event.
    o.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    if (!o.hEvent) {
        CrAssert(0, CrWin32ErrorString());
    }

    HANDLE h[2] = {o.hEvent, h_TermEvent};

    do
    {
        bPending = ReadDirectoryChangesW(m_hDIR,
            &buffer[0], buffer.size(),
            TRUE, FILE_NOTIFY_CHANGE_LAST_WRITE,
            &dwBytes, &o, NULL);
        if (!bPending)
        {
            CrAssert(0, CrWin32ErrorString());
        }

        switch (WaitForMultipleObjects(2, h, FALSE, INFINITE))
        {
            case WAIT_OBJECT_0:
            {
                if (!GetOverlappedResult(m_hDIR, &o, &dwBytes, TRUE)) {
                    CrAssert(0, CrWin32ErrorString());
                }

                bPending = false;

                if (dwBytes == 0)
                    break;

                FILE_NOTIFY_INFORMATION *fni = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(&buffer[0]);
                do
                {
                    if (fni->Action != 0)
                    {
                        std::wstring fileName(fni->FileName, fni->FileNameLength);
                        m_Callback(fileName);
                    }

                    if (fni->NextEntryOffset == 0)
                         break;

                    fni = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(reinterpret_cast<BYTE*>(fni) + fni->NextEntryOffset);
                }
                while (true);

                break;
            }

            case WAIT_OBJECT_0+1:
                bKeepRunning = false;
                break;

            case WAIT_FAILED:
                CrAssert(0, CrWin32ErrorString());
                break;
        }
    }
    while (bKeepRunning);

    if (bPending)
    {
        CancelIo(m_hDIR);
        GetOverlappedResult(m_hDIR, &o, &dwBytes, TRUE);
    }

    CloseHandle(o.hEvent);
}

【讨论】:

  • 为什么要关闭 hEvent?
  • @mutex36:所以它没有泄露。打开的内核句柄在进程终止期间会自动关闭,但谁说线程只能在应用程序关闭期间停止?线程创建事件对象,它应该在退出之前释放对象,不管线程是如何被使用的。
  • 出于好奇:如果您不 GetOverlappedResult() 剩余/未决数据会怎样?
  • @mutex36:它将位于打开的目录对象的 I/O 队列中,直到检索到或直到队列/目录关闭。
  • @mutex36 你应该总是读取异步操作的结果,即使它被取消了。由于它是异步的,因此您不知道它是否仍在运行,并且不应撕掉它仍在引用的任何内容。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-11-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-16
  • 2011-09-04
相关资源
最近更新 更多