如何避免在GetFileAttributes中出现网络卡顿?

14

我正在测试远程共享(在Windows服务器上)中文件的存在性。用于测试的底层函数是WinAPI的GetFileAttributes,这个函数在各种情况下可能需要很长时间(几十秒),比如目标服务器离线,存在权限或DNS问题等。

然而,在我的情况下,总是局域网访问,所以如果文件不能在1秒内访问,则通常等待数十秒后也无法访问...

是否有一种替代GetFileAttributes但不会停顿的方法?(除了在线程中调用它并在超时后终止线程,这似乎会带来自己的一些问题)


除了异步模型,我想不到其他的东西。 - Umair Ahmed
在其他情况下,我通过部署极简Web服务器来解决这个问题,以便提供共享文件,因为HTTP请求可以轻松地被取消或超时。但在这种情况下,由于存在各种原因(部署困难、安全问题等),这不是一个解决方案。 - Eric Grange
3个回答

8
问题并不在于GetFileAttributes函数本身,它通常只使用底层文件系统驱动程序的一个调用。问题出在IO操作上,这导致了阻塞。
不过,解决方案很简单。在一秒后调用CancelSynchronousIo()函数(这显然需要第二个线程,因为第一个线程被卡在GetFileAttributes函数内部)。请参考CancelSynchronousIo()

1
请注意,CancelSynchronousIo在Windows XP中不可用。 - Josh Kelley
@MSalters: 当我使用GetFileAttributes方法时,我遇到了访问被拒绝(5)的错误。我的服务器是Windows 2003,硬件配置较低。我在其他禁用权限的系统上尝试了相同的调用,结果完美运行。慢IO是否会导致“访问被拒绝”错误。 - Rahul KP
@RahulKP:相当不可能。 - MSalters
1
值得一提的是,从多媒体定时器(timeSetEvent)调用CancelSynchronousIo()似乎可以正常工作,因此代码只需要设置定时器,调用GetFileAttributes,并在“finally”块中取消定时器。 - Eric Grange

4

委托的一个很酷的功能是,您始终可以使用 BeginInvokeEndInvoke。 只要确保被调用的方法不会抛出异常,因为 [我相信] 这会导致崩溃(未处理的异常)。

AttributeType attributes = default(AttributeType);

Action<string> helper =
    (path) =>
    {
        try
        {
            // GetFileAttributes
            attributes = result;
        }
        catch
        {
        }
    };
IAsyncResult asyncResult = helper.BeginInvoke();
// whatever
helper.EndInvoke();
// at this point, the attributes local variable has a valid value.

1
那么基本上,除了将API调用包装在线程中,没有其他希望了吗?我希望能够找到一种不需要使用线程的解决方案,因为在超时时终止线程并不是“干净”的(根据经验,可能会发生糟糕的事情),而忽略停滞的线程可能会导致大量的停滞线程... - Eric Grange
抱歉,我之前错误地假设你正在使用.NET(在此之前回答了几个相关问题)。如果API没有提供异步和/或超时版本,则线程解决方案可能是唯一可靠的解决方案。 - Sam Harwell

0

我认为你最好的解决方案是使用线程池线程来执行工作。

  • 分配一个工作单元来查询文件的属性
  • GetFileAttributes运行完成
  • 将结果返回到你的表单
  • 当你的线程函数完成时,线程会自动返回到池中(无需杀死它)

通过使用线程池,您可以节省创建新线程的成本。
而且你也不必费力去摆脱它们。

然后你有一个方便的帮助方法,它可以在线程池线程上运行对象的方法过程,使用QueueUserWorkItem

RunInThreadPoolThread(
      GetFileAttributesThreadMethod, 
      TGetFileAttributesData.Create('D:\temp\foo.xml', Self.Handle), 
      WT_EXECUTEDEFAULT);

你创建一个对象来保存线程数据信息:

TGetFileAttributesData = class(TObject)
public
    Filename: string;
    WndParent: HWND;
    Attributes: DWORD;
    constructor Create(Filename: string; WndParent: HWND);
end;

然后你创建你的线程回调方法:

procedure TForm1.GetFileAttributesThreadMethod(Data: Pointer);
var
    fi: TGetFileAttributesData;
begin
    fi := TObject(Data) as TGetFileAttributesData;
    if fi = nil then
        Exit;

    fi.attributes := GetFileAttributes(PWideChar(fi.Filename));

    PostMessage(fi.WndParent, WM_GetFileAttributesComplete, NativeUInt(Data), 0);
end;

然后你只需要处理这个消息:

procedure WMGetFileAttributesComplete(var Msg: TMessage); message WM_GetFileAttributesComplete;

procedure TfrmMain.WMGetFileAttributesComplete(var Msg: TMessage);
var
    fi: TGetFileAttributesData;
begin
    fi := TObject(Pointer(Msg.WParam)) as TGetFileAttributesData;
    try
        ShowMessage(Format('Attributes of "%s": %.8x', [fi.Filename, fi.attributes]));
    finally
        fi.Free;
    end;
end;

RunInThreadPoolThread 这个神奇的东西只是一个小小的工具,它可以让你在一个线程中执行实例方法:

这只是一个包装器,让你可以调用实例变量上的方法:

TThreadMethod = procedure (Data: Pointer) of object;

TThreadPoolCallbackContext = class(TObject)
public
    ThreadMethod: TThreadMethod;
    Context: Pointer;
end;

function ThreadPoolCallbackFunction(Parameter: Pointer): Integer; stdcall;
var
    tpContext: TThreadPoolCallbackContext;
begin
    try
        tpContext := TObject(Parameter) as TThreadPoolCallbackContext;
    except
        Result := -1;
        Exit;
    end;
    try
        tpContext.ThreadMethod(tpContext.Context);
    finally
        try
            tpContext.Free;
        except
        end;
    end;
    Result := 0;
end;

function RunInThreadPoolThread(const ThreadMethod: TThreadMethod; const Data: Pointer; Flags: ULONG): BOOL;
var
    tpContext: TThreadPoolCallbackContext;
begin
    {
        Unless you know differently, the flag you want to use is 0 (WT_EXECUTEDEFAULT).

        If your callback might run for a while you can pass the WT_ExecuteLongFunction flag.
                Sure, I'm supposed to pass WT_EXECUTELONGFUNCTION if my function takes a long time, but how long is long?
                http://blogs.msdn.com/b/oldnewthing/archive/2011/12/09/10245808.aspx

        WT_EXECUTEDEFAULT (0):
                By default, the callback function is queued to a non-I/O worker thread.
                The callback function is queued to a thread that uses I/O completion ports, which means they cannot perform
                an alertable wait. Therefore, if I/O completes and generates an APC, the APC might wait indefinitely because
                there is no guarantee that the thread will enter an alertable wait state after the callback completes.
        WT_EXECUTELONGFUNCTION (0x00000010):
                The callback function can perform a long wait. This flag helps the system to decide if it should create a new thread.
        WT_EXECUTEINPERSISTENTTHREAD (0x00000080)
                The callback function is queued to a thread that never terminates.
                It does not guarantee that the same thread is used each time. This flag should be used only for short tasks
                or it could affect other timer operations.
                This flag must be set if the thread calls functions that use APCs.
                For more information, see Asynchronous Procedure Calls.
                Note that currently no worker thread is truly persistent, although worker threads do not terminate if there
                are any pending I/O requests.
    }

    tpContext := TThreadPoolCallbackContext.Create;
    tpContext.ThreadMethod := ThreadMethod;
    tpContext.Context := Data;

    Result := QueueUserWorkItem(ThreadPoolCallbackFunction, tpContext, Flags);
end;
读者练习:在GetFileAttributesData对象内创建一个Cancelled标志,告诉线程必须释放数据对象并且不要向父级发送消息。

这一切都是为了表达你正在创建的内容:

DWORD WINAPI GetFileAttributes(
  _In_      LPCTSTR                         lpFileName,
  _Inout_   LPOVERLAPPED                    lpOverlapped,
  _In_      LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

使用池化线程并不能解决线程可能因为GetFileAttributes等待网络超时而被阻塞很长时间的问题。此外,还需要忽略不相关的线程。例如,如果您在10秒内两次查询同一文件,则第一次调用可能会被卡住30秒,而第二次可能立即成功(您需要忽略第一次调用的结果)。此外,如果您以几秒钟的频率监视多个文件,很容易出现数十甚至数百个阻塞的线程...根本不实用 :/ - Eric Grange
忽略无关的线程的解决方法与Windows本来就提供了一个重叠(即异步)的GetFileAttributesEx版本时相同 - 你必须取消现有的调用。这个问题已经被解决了。你的担心是当你有几十个或几百个停滞的线程时该怎么办。我认为这不是一个问题,因为用户工作项队列会将项目排队,直到旧项目从队列中清除。尽管如此,你可以做任何事情来帮助它,以便更快地将线程返回到池中。 - Ian Boyd
值得注意的是,QueueUserWorkItem 会将您的项目排队,而不是创建数百个线程。 QueueUserWorkItem 的目的之一是让您排队工作项 - 线程池决定何时执行它们。 - Ian Boyd
如果队列有限制大小,那么阻塞的调用将会阻止(或延迟)其他本来可以快速完成的调用,从而造成瓶颈。在多媒体定时器中调用CancelSynchronousIo()似乎是有效的,可以在GetFileAttributes之后的“finally”块中取消定时器。 - Eric Grange

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接