【问题标题】:How to avoid network stalls in GetFileAttributes?如何避免 GetFileAttributes 中的网络停顿?
【发布时间】:2010-11-11 15:23:36
【问题描述】:

我正在测试远程共享中的文件是否存在(在 Windows 服务器上)。用于测试的底层函数是 WinAPI 的 GetFileAttributes,发生的情况是该函数在各种情况下会花费过多的时间(几十秒),例如当目标服务器离线时,当有权限或 DNS 问题等。

但是,在我的特定情况下,它始终是 LAN 访问,因此如果在不到 1 秒的时间内无法访问该文件,则通常需要再等待数十秒才能访问它...

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

【问题讨论】:

  • 除了异步模型什么都想不到。
  • 在其他情况下,我通过部署一个极简网络服务器来提供共享文件解决了这个问题,因为 HTTP 请求很容易被取消/超时。但在这种情况下,由于各种原因(部署问题、安全问题等),这不是解决方案。

标签: windows networking


【解决方案1】:

真正的问题不在于 GetFileAttributes。它通常只使用对底层文件系统驱动程序的一次调用。是那个 IO 停滞不前。

不过,解决方案可能很简单。一秒钟后调用CancelSynchronousIo()(这显然需要第二个线程,因为您的第一个线程卡在GetFileAttributes中)。

【讨论】:

  • 请注意,CancelSynchronousIo 在 Windows XP 中不可用。
  • @MSalters:使用 GetFileAttributes 方法时出现访问被拒绝 (5) 错误。我有硬件配置较低的 Window 2003 服务器。我在其他运行良好的系统上尝试了相同的调用,但禁用了权限。会减慢 IO 导致“访问被拒绝”错误。
  • @RahulKP:不太可能。
  • FWIW 从多媒体计时器 (timeSetEvent) 调用 CancelSynchronousIo() 似乎工作正常,因此代码只需要设置计时器、调用 GetFileAttributes 并在“finally”块中取消计时器。
【解决方案2】:

关于代表的一个很酷的事情是您始终可以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.

【讨论】:

  • 所以基本上,除了将 API 调用包装在一个线程中之外,没有希望了吗?我希望有一个线程之外的解决方案,因为在超时时终止线程并不是“干净的”(根据经验,可能会发生坏事),并且忽略停滞的线程可能会导致大量停滞的线程......
  • 抱歉,我似乎也误以为您在 .NET 中工作(在此之前回答了其中的一些问题)。如果 API 没有可用的异步和/或超时版本,那么线程解决方案可能是唯一可靠的解决方案。
【解决方案3】:

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

  • 分配一个工作单元来查询文件的属性
  • 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 秒,而第二次可能会立即成功(并且您需要忽略第一次调用的结果)。此外,如果您以几秒钟的频率监视多个文件,那么最终会“容易”出现数​​十个甚至数百个停滞的线程......根本不切实际:/
  • 忽略不相关线程的解决方法与 Windows 已经本机提供重叠(即异步)GetFileAttributesEx 版本相同 - 您必须取消现有调用。这是通过练习来解决的。您关心的是当您有数十或数百个停滞线程时该怎么办。我会提交这不是一个问题,因为用户工作项队列会将项目排队,直到旧项目从队列中刷新。虽然,anything you can do to help it along 会更快地将线程返回到池中。
  • 不要让QueueUserWorkItem 排队您的项目也很有用;而不是创建数百个线程。 QueueUserWorkItem 的目的之一是让您排队工作项 - 线程池决定它们何时执行。
  • 如果队列的大小有限,那么停滞的呼叫将阻止(或延迟)其他呼叫(否则可能会很快),因此这会造成瓶颈。在多媒体计时器中调用 CancelSynchronousIo() 似乎可以正常工作 AFAICT,并且可以在 GetFileAttributes 之后的“finally”块中取消计时器。
猜你喜欢
  • 2023-03-04
  • 2011-01-10
  • 1970-01-01
  • 1970-01-01
  • 2019-09-20
  • 2016-10-05
  • 1970-01-01
  • 1970-01-01
  • 2023-02-10
相关资源
最近更新 更多