【问题标题】:Using ReadDirectoryChangesW asynchronously in a loop在循环中异步使用 ReadDirectoryChangesW
【发布时间】:2016-11-14 16:31:36
【问题描述】:

简介:

我正在尝试在循环中异步使用ReadDirectoryChangesW

下面的 sn-p 说明了我想要实现的目标:

DWORD example()
{
    DWORD error = 0;

    OVERLAPPED ovl = { 0 };
    ovl.hEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL);

    if (NULL == ovl.hEvent) return ::GetLastError();

    char buffer[1024];

    while(1)
    {
        process_list_of_existing_files();

        error = ::ReadDirectoryChangesW(
            m_hDirectory, // I have added FILE_FLAG_OVERLAPPED in CreateFile
            buffer, sizeof(buffer), FALSE,
            FILE_NOTIFY_CHANGE_FILE_NAME,
            NULL, &ovl, NULL);

        // we have new files, append them to the list
        if(error) append_new_files_to_the_list(buffer);
        // just continue with the loop
        else if(::GetLastError() == ERROR_IO_PENDING) continue;
        // RDCW error, this is critical -> exit
        else return ::GetLastError(); 
    }
}

问题:

ReadDirectoryChangesW返回FALSEGetLastError()代码为ERROR_IO_PENDING时,我不知道如何处理。

在这种情况下,我应该继续循环并继续循环,直到 ReadDirectoryChangesW 返回 buffer 我可以处理。

我为解决这个问题所做的努力:

我曾尝试使用WaitForSingleObject(ovl.hEvent, 1000),但它因错误1450 ERROR_NO_SYSTEM_RESOURCES 而崩溃。以下是重现此行为的 MVCE:

#include <iostream>
#include <Windows.h>

DWORD processDirectoryChanges(const char *buffer)
{
    DWORD offset = 0;
    char fileName[MAX_PATH] = "";
    FILE_NOTIFY_INFORMATION *fni = NULL;

    do
    {
        fni = (FILE_NOTIFY_INFORMATION*)(&buffer[offset]);
        // since we do not use UNICODE, 
        // we must convert fni->FileName from UNICODE to multibyte
        int ret = ::WideCharToMultiByte(CP_ACP, 0, fni->FileName,
            fni->FileNameLength / sizeof(WCHAR),
            fileName, sizeof(fileName), NULL, NULL);

        switch (fni->Action)
        {
        case FILE_ACTION_ADDED:     
        {
            std::cout << fileName << std::endl;
        }
        break;
        default:
            break;
        }

        ::memset(fileName, '\0', sizeof(fileName));
        offset += fni->NextEntryOffset;

    } while (fni->NextEntryOffset != 0);

    return 0;
}

int main()
{
    HANDLE hDir = ::CreateFile("C:\\Users\\nenad.smiljkovic\\Desktop\\test", 
        FILE_LIST_DIRECTORY,
        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        NULL, OPEN_EXISTING, 
        FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL);

    if (INVALID_HANDLE_VALUE == hDir) return ::GetLastError();

    OVERLAPPED ovl = { 0 };
    ovl.hEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL);

    if (NULL == ovl.hEvent) return ::GetLastError();

    DWORD error = 0, br;
    char buffer[1024];

    while (1)
    {
        error = ::ReadDirectoryChangesW(hDir,
            buffer, sizeof(buffer), FALSE,
            FILE_NOTIFY_CHANGE_FILE_NAME,
            NULL, &ovl, NULL);

        if (0 == error)
        {
            error = ::GetLastError();

            if (ERROR_IO_PENDING != error)
            {
                ::CloseHandle(ovl.hEvent);
                ::CloseHandle(hDir);
                return error;
            }
        }

        error = ::WaitForSingleObject(ovl.hEvent, 0);

        switch (error)
        {
        case WAIT_TIMEOUT:
            break;
        case WAIT_OBJECT_0:
        {
            error = processDirectoryChanges(buffer);

            if (error > 0)
            {
                ::CloseHandle(ovl.hEvent);
                ::CloseHandle(hDir);
                return error;
            }

            if (0 == ::ResetEvent(ovl.hEvent))
            {
                error = ::GetLastError();
                ::CloseHandle(ovl.hEvent);
                ::CloseHandle(hDir);
                return error;
            }
        }
        break;
        default:
            error = ::GetLastError();
            ::CloseHandle(ovl.hEvent);
            ::CloseHandle(hDir);
            return error;
            break;
        }
    }

    return 0;
}

通读文档,我似乎需要GetOverlappedResult,最后一个参数设置为FALSE,但我不知道如何正确使用这个API。

问题:

由于 MVCE 很好地说明了我正在尝试做什么(打印新添加文件的名称),您能否告诉我必须在 while 循环中修复哪些内容才能使其正常工作?

同样,重点是在循环中异步使用ReadDirectoryChangesW,如 INTRODUCTION 中的 sn-p 所示。

【问题讨论】:

  • 该代码没有多大意义。它爆炸了,因为您使用 0 超时而不是 INFINITE,因此您实际上并没有等待并以非常高的速率调用 ReadDirectoryChangesW。仅当您有其他事情要做时才使用 OVERLAPPED。所以你可以使用 WaitForMultipleObjects()。如发布的那样,您不应该使用它。
  • @HansPassant:如果 RDCW 没有返回我可以使用的缓冲区,我想 continue 循环,执行其他代码(从顶部开始 foo()),直到我得到填充缓冲区。那是我等了0秒的地方。如果没有可用的内容,则循环继续,否则我处理缓冲区内容。抱歉,我的母语不是英语,所以如果我的意图在第一次阅读时不清楚,我可以理解。
  • @AlwaysLearningNewStuff 如果 RDCW 返回 IO Pending 错误,则在首先发出重叠事件信号之前不要再次调用 RDCW。在此期间,您可以继续循环并做其他事情。在显示的代码中,在进入循环之前调用 RDCW 一次,然后在 WAIT_OBJECT_0 处理程序中再次调用它。
  • 如果你像这样在一个紧密的循环中运行它,为什么还要使用异步呢?只需使用阻塞调用。
  • @JonathanPotter:根据前面的答案的讨论,我相信他想在循环期间执行后台任务,即使 MCVE 没有显示这一点。

标签: c++ winapi asynchronous readdirectorychangesw


【解决方案1】:

您的程序的基本结构看起来或多或少没问题,您只是错误地使用了异步 I/O 调用。每当没有新文件时,事件句柄的等待就会立即超时,这很好,但是您随后会发出一个全新的 I/O 请求,但事实并非如此。

这就是您的系统资源不足的原因;您正在发出完全倾斜的 I/O 请求,而无需等待其中任何一个完成。您应该只在现有请求完成后发出新请求。

(另外,你应该调用 GetOverlappedResult 来检查 I/O 是否成功。)

所以你的循环应该看起来更像这样:

    ::ReadDirectoryChangesW(hDir,
        buffer, sizeof(buffer), FALSE,
        FILE_NOTIFY_CHANGE_FILE_NAME,
        NULL, &ovl, NULL);

    while (1)
    {
        DWORD dw;
        DWORD result = ::WaitForSingleObject(ovl.hEvent, 0);

        switch (result)
        {
        case WAIT_TIMEOUT:

            processBackgroundTasks();

            break;

        case WAIT_OBJECT_0:

            ::GetOverlappedResult(hDir, &ovl, &dw, FALSE);

            processDirectoryChanges(buffer);

            ::ResetEvent(ovl.hEvent);

            ::ReadDirectoryChangesW(hDir,
                buffer, sizeof(buffer), FALSE,
                FILE_NOTIFY_CHANGE_FILE_NAME,
                NULL, &ovl, NULL);

            break;
        }
    }

注意事项:

  • 为简单起见,省略了错误处理;我没有做任何测试或检查你的代码是否有任何其他问题。

  • 如果可能没有要执行的任何后台任务,您应该针对这种情况进行测试并将超时设置为 INFINITE 而不是 0,否则您将处于旋转状态。

  • 我只想展示使其工作所需的最小更改,但调用 WaitForSingleObject 后跟 GetOverlappedResult 是多余的;对 GetOverlappedResult 的一次调用既可以检查 I/O 是否完成,也可以检索结果。


根据要求,修改后的版本仅使用 GetOverlappedResult 并具有最少的错误检查。我还添加了一个示例,说明您可以如何处理已无事可做的情况;如果您对文件执行的任何处理确实会永远运行,那么您就不需要那个位了。

    ::ResetEvent(ovl.hEvent);

    if (!::ReadDirectoryChangesW(hDir,
        buffer, sizeof(buffer), FALSE,
        FILE_NOTIFY_CHANGE_FILE_NAME,
        NULL, &ovl, NULL))
    {
       error = GetLastError();
       if (error != ERROR_IO_PENDING) fail();
    }

    while (1)
    {
        BOOL wait;

        result = process_list_of_existing_files();

        if (result == MORE_WORK_PENDING)
        {
           wait = FALSE;
        } 
        else if (result == NO_MORE_WORK_PENDING)
        {
           wait = TRUE;
        } 

        if (!::GetOverlappedResult(hDir, &ovl, &dw, wait))
        {
           error = GetLastError();
           if (error == ERROR_IO_INCOMPLETE) continue;
           fail();
        }

        processDirectoryChanges(buffer);

        ::ResetEvent(ovl.hEvent);

        if (!::ReadDirectoryChangesW(hDir,
            buffer, sizeof(buffer), FALSE,
            FILE_NOTIFY_CHANGE_FILE_NAME,
            NULL, &ovl, NULL))
        {
           error = GetLastError();
           if (error != ERROR_IO_PENDING) fail();
        } 
    }

【讨论】:

  • 异步调用 ReadDirectoryChangesW 然后 WaitForSingleObject - 几乎杀死异步行为。这种循环只有一个优点 - processBackgroundTasks(); 除了这个 - 我们可以同步调用 ReadDirectoryChangesW 以达到相同的效果。但看起来像 OP 的主要问题 - 他根本不明白异步是如何工作的,如何将上下文绑定到操作然后在回调中使用..
  • 感谢您的回答。关于您的第二次观察,我要实现的目标显示在 INTRODUCTION 的代码 sn-p 中。关于您的第三次观察,您能否编辑您的答案以显示如何正确使用GetOverlappedResult?我真的很想做到这一点。关于错误检查,我需要处理它们,所以如果您至少可以处理 ERROR_IO_PENDING 就足够了。再次感谢您的回答。
  • 如果没有描述process_list_of_existing_files(); 的实际作用,sn-p 并不是很有用。如果总是有工作要做,你不必担心旋转,只是这似乎不太可能 - 如果用户停止添加新文件,你肯定会最终 完成你在现有的工作上所做的一切?
  • 如果用户停止添加新文件,你最终会完成对现有文件所做的一切吗? 只要应用程序正常工作process_list_of_existing_files();将被调用。用户应按 Ctrl + C 退出应用程序,否则它将永远运行。由于您的原始答案+来自用户Remy Lebeau的提示,我提出了与您类似的解决方案(来自编辑)。无需使用该代码更新我的 OP,我只会接受您的回答。我也会赞成你的回答。谢谢。
  • dw 变量在这个答案中代表什么?
【解决方案2】:

使用 IOCP 的间接变体

  1. 创建一个类/结构继承(包含)OVERLAPPED(或 IO_STATUS_BLOCK),一个引用计数器,目录句柄和数据 你需要
  2. 致电BindIoCompletionCallback (RtlSetIoCompletionCallback) 目录句柄,用于设置回调
  3. 有一个DoRead() 例程,我们将首先从主线程调用,然后从回调中调用
  4. DoRead() 中,每次调用ReadDirectoryChangesW 之前 AddRef(); 因为我们将引用(通过OVERLAPPED)传递给我们的 结构到内核
  5. Main(比如 GUI 线程)在初始调用后可以继续执行自己的任务 到DoRead(),与 APC 变体不同,我们不需要等待警报状态
  6. 在回调中,我们从继承(包含)中获得了指向我们的结构的指针 OVERLAPPED。如果需要,执行任何任务 (processDirectoryChanges) 继续间谍 - 打电话给DoRead(),最后打电话给Release()
  7. 如果来自DoRead()ReadDirectoryChangesW 失败(结果将没有回调) - 我们需要直接调用回调 带有错误代码
  8. 为了停止,我们可以简单地关闭目录句柄 - 结果,我们得到 STATUS_NOTIFY_CLEANUP 在回调中

===================================

//#define _USE_NT_VERSION_

class SPYDATA : 
#ifdef _USE_NT_VERSION_
    IO_STATUS_BLOCK
#else
    OVERLAPPED 
#endif
{
    HANDLE _hFile;
    LONG _dwRef;
    union {
        FILE_NOTIFY_INFORMATION _fni;
        UCHAR _buf[PAGE_SIZE];
    };

    void DumpDirectoryChanges()
    {
        union {
            PVOID buf;
            PBYTE pb;
            PFILE_NOTIFY_INFORMATION pfni;
        };

        buf = _buf;

        for (;;)
        {
            DbgPrint("%x <%.*S>\n", pfni->Action, pfni->FileNameLength >> 1, pfni->FileName);

            ULONG NextEntryOffset = pfni->NextEntryOffset;

            if (!NextEntryOffset)
            {
                break;
            }

            pb += NextEntryOffset;
        }
    }

#ifdef _USE_NT_VERSION_
    static VOID WINAPI _OvCompRoutine(
        _In_    NTSTATUS dwErrorCode,
        _In_    ULONG_PTR dwNumberOfBytesTransfered,
        _Inout_ PIO_STATUS_BLOCK Iosb
        )
    {
        static_cast<SPYDATA*>(Iosb)->OvCompRoutine(dwErrorCode, (ULONG)dwNumberOfBytesTransfered);
    }
#else
    static VOID WINAPI _OvCompRoutine(
        _In_    DWORD dwErrorCode, // really this is NTSTATUS
        _In_    DWORD dwNumberOfBytesTransfered,
        _Inout_ LPOVERLAPPED lpOverlapped
        )
    {
        static_cast<SPYDATA*>(lpOverlapped)->OvCompRoutine(dwErrorCode, dwNumberOfBytesTransfered);
    }
#endif

    VOID OvCompRoutine(NTSTATUS status, DWORD dwNumberOfBytesTransfered)
    {
        DbgPrint("[%x,%x]\n", status, dwNumberOfBytesTransfered);

        if (0 <= status) 
        {
            if (status != STATUS_NOTIFY_CLEANUP)
            {
                if (dwNumberOfBytesTransfered) DumpDirectoryChanges();
                process_list_of_existing_files();// so hard do this here ?!?
                DoRead();
            }
            else
            {
                DbgPrint("\n---- NOTIFY_CLEANUP -----\n");
            }
        }

        Release();
        MyReleaseRundownProtection();
    }

    ~SPYDATA()
    {
        Cancel();
    }

public:

    void DoRead()
    {
        if (MyAcquireRundownProtection())
        {
            AddRef();
#ifdef _USE_NT_VERSION_
            NTSTATUS status = ZwNotifyChangeDirectoryFile(_hFile, 0, 0, this, this, &_fni, sizeof(_buf), FILE_NOTIFY_VALID_MASK, TRUE);
            if (NT_ERROR(status))
            {
                OvCompRoutine(status, 0);
            }
#else
            if (!ReadDirectoryChangesW(_hFile, _buf, sizeof(_buf), TRUE, FILE_NOTIFY_VALID_MASK, (PDWORD)&InternalHigh, this, 0))
            {
                OvCompRoutine(RtlGetLastNtStatus(), 0);
            }
#endif
        }
    }

    SPYDATA()
    {
        _hFile = 0;// ! not INVALID_HANDLE_VALUE because use ntapi for open file
        _dwRef = 1;
#ifndef _USE_NT_VERSION_
        RtlZeroMemory(static_cast<OVERLAPPED*>(this), sizeof(OVERLAPPED));
#endif
    }

    void AddRef()
    {
        InterlockedIncrement(&_dwRef);
    }

    void Release()
    {
        if (!InterlockedDecrement(&_dwRef))
        {
            delete this;
        }
    }

    BOOL Create(POBJECT_ATTRIBUTES poa)
    {
        IO_STATUS_BLOCK iosb;
        NTSTATUS status = ZwOpenFile(&_hFile, FILE_GENERIC_READ, poa, &iosb, FILE_SHARE_VALID_FLAGS, FILE_DIRECTORY_FILE);
        if (0 <= status)
        {
            return
#ifdef _USE_NT_VERSION_
                0 <= RtlSetIoCompletionCallback(_hFile, _OvCompRoutine, 0);
#else
                BindIoCompletionCallback(_hFile, _OvCompRoutine, 0);
#endif
        }
        return FALSE;
    }

    void Cancel()
    {
        if (HANDLE hFile = InterlockedExchangePointer(&_hFile, 0))
        {
            NtClose(hFile);
        }
    }
};

void DemoF()
{
    if (MyInitializeRundownProtection())
    {
        STATIC_OBJECT_ATTRIBUTES(oa, "<SOME_DIRECTORY>");

        if (SPYDATA* p = new SPYDATA)
        {
            if (p->Create(&oa))
            {
                p->DoRead();
            }

            //++ GUI thread run
            MessageBoxW(0, L"wait close program...", L"", MB_OK);
            //-- GUI thread end

            p->Cancel();

            p->Release();
        }

        MyWaitForRundownProtectionRelease();
    }
}

【讨论】:

  • 我看不到 IOCP/ACP 如何适合我的任务。如果您查看 INTRODUCTION 中的代码 sn-p,整个混乱都围绕着这一点展开:如果有新文件添加到目录中,我需要在处理现有文件列表后立即知道。我不明白 IOCP/ACP 如何在那一刻告诉我目录中是否有新文件。你能帮我理解吗?谢谢
  • @AlwaysLearningNewStuff - 你的任务也是如此(处理现有文件的列表) - 在回调中。在什么问题上?
  • @AlwaysLearningNewStuff - 一次又一次 - 你完全不了解异步编程,这种情况也是如此。只有这不适合你的任务
  • 列表处理必须始终工作,而不仅仅是在触发回调时。如果我理解你的话,列表处理只会在调用回调时运行(添加新文件时)。我认为这可行的唯一方法是在回调中复制 while 循环中的代码。
  • @AlwaysLearningNewStuff - “列表处理必须始终工作” - 你的意思是什么?如果您需要做一些独立于目录更改事件的任务 - 直接这样做,有什么问题?同步?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-07-29
  • 2023-03-19
  • 2018-09-18
  • 2018-10-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多