是否可以使用 AsyncCalls 单元创建线程池?

发布于 2024-11-01 16:07:36 字数 3536 浏览 0 评论 0原文

我正在尝试使用 异步调用。理想情况下,我希望它同时执行 10 次以上的查找,但目前它一次只执行 1 次查找。我在这里做错了什么?

我的表单包含 1 个按钮和 1 个备忘录。

unit main;

interface

uses
  Windows,
  Messages,
  SysUtils,
  Classes,
  Forms,
  StdCtrls,
  AsyncCalls,
  IdGlobal,
  IdUDPClient,
  Controls;

type
  PWMUCommand = ^TWMUCommand;

  TWMUCommand = record
    host: string;
    ip: string;
    bOnline: boolean;
  end;

type
  PNetbiosTask = ^TNetbiosTask;

  TNetbiosTask = record
    hMainForm: THandle;
    sAddress: string;
    sHostname: string;
    bOnline: boolean;
    iTimeout: Integer;
  end;

const
  WM_THRD_SITE_MSG  = WM_USER + 5;
  WM_POSTED_MSG     = WM_USER + 8;

type
  TForm2 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
  private
    procedure ThreadMessage(var Msg: TMessage); message WM_POSTED_MSG;
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form2             : TForm2;

implementation

{$R *.dfm}

function NetBiosLookup(Data: TNetbiosTask): boolean;
const
  NB_REQUEST        = #$A2#$48#$00#$00#$00#$01#$00#$00 +
    #$00#$00#$00#$00#$20#$43#$4B#$41 +
    #$41#$41#$41#$41#$41#$41#$41#$41 +
    #$41#$41#$41#$41#$41#$41#$41#$41 +
    #$41#$41#$41#$41#$41#$41#$41#$41 +
    #$41#$41#$41#$41#$41#$00#$00#$21 +
    #$00#$01;

  NB_PORT           = 137;
  NB_BUFSIZE        = 8192;
var
  Buffer            : TIdBytes;
  I                 : Integer;
  RepName           : string;
  UDPClient         : TIdUDPClient;
  msg_prm           : PWMUCommand;
begin
  RepName := '';
  Result := False;
  UDPClient := nil;

  UDPClient := TIdUDPClient.Create(nil);
  try
    try
      with UDPClient do
      begin
        Host := Trim(Data.sAddress);
        Port := NB_PORT;

        Send(NB_REQUEST);
      end;

      SetLength(Buffer, NB_BUFSIZE);
      if (0 < UDPClient.ReceiveBuffer(Buffer, Data.iTimeout)) then
      begin

        for I := 1 to 15 do
          RepName := RepName + Chr(Buffer[56 + I]);

        RepName := Trim(RepName);
        Data.sHostname := RepName;

        Result := True;
      end;

    except
      Result := False;
    end;
  finally
    if Assigned(UDPClient) then
      FreeAndNil(UDPClient);
  end;

  New(msg_prm);
  msg_prm.host := RepName;
  msg_prm.ip := Data.sAddress;
  msg_prm.bOnline := Length(RepName) > 0;

  PostMessage(Data.hMainForm, WM_POSTED_MSG, WM_THRD_SITE_MSG, integer(msg_prm));

end;

procedure TForm2.Button1Click(Sender: TObject);
var
  i                 : integer;
  ArrNetbiosTasks   : array of TNetbiosTask;
  sIp               : string;
begin
  //

  SetMaxAsyncCallThreads(50);

  SetLength(ArrNetbiosTasks, 255);
  sIp := '192.168.1.';
  for i := 1 to 255 do
  begin

    ArrNetbiosTasks[i - 1].hMainForm := Self.Handle;
    ArrNetbiosTasks[i - 1].sAddress := Concat(sIp, IntToStr(i));
    ArrNetbiosTasks[i - 1].iTimeout := 5000;

    AsyncCallEx(@NetBiosLookup, ArrNetbiosTasks[i - 1]);
    Application.ProcessMessages;
  end;
end;

procedure TForm2.ThreadMessage(var Msg: TMessage);
var
  msg_prm           : PWMUCommand;
begin
  //
  case Msg.WParam of
    WM_THRD_SITE_MSG:
      begin
        msg_prm := PWMUCommand(Msg.LParam);
        try
          Memo1.Lines.Add(msg_prm.ip + ' = ' + msg_prm.host + ' --- Online? ' + BoolToStr(msg_prm.bOnline));
        finally
          Dispose(msg_prm);
        end;
      end;
  end;

end;

end.

I am attempting to perform a Netbios lookup on an entire class C subnet using AsyncCalls. Ideally I'd like it to perform 10+ lookups concurrently but it currently only does 1 lookup at a time. What am I doing wrong here?

My form contains 1 button and 1 memo.

unit main;

interface

uses
  Windows,
  Messages,
  SysUtils,
  Classes,
  Forms,
  StdCtrls,
  AsyncCalls,
  IdGlobal,
  IdUDPClient,
  Controls;

type
  PWMUCommand = ^TWMUCommand;

  TWMUCommand = record
    host: string;
    ip: string;
    bOnline: boolean;
  end;

type
  PNetbiosTask = ^TNetbiosTask;

  TNetbiosTask = record
    hMainForm: THandle;
    sAddress: string;
    sHostname: string;
    bOnline: boolean;
    iTimeout: Integer;
  end;

const
  WM_THRD_SITE_MSG  = WM_USER + 5;
  WM_POSTED_MSG     = WM_USER + 8;

type
  TForm2 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
  private
    procedure ThreadMessage(var Msg: TMessage); message WM_POSTED_MSG;
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form2             : TForm2;

implementation

{$R *.dfm}

function NetBiosLookup(Data: TNetbiosTask): boolean;
const
  NB_REQUEST        = #$A2#$48#$00#$00#$00#$01#$00#$00 +
    #$00#$00#$00#$00#$20#$43#$4B#$41 +
    #$41#$41#$41#$41#$41#$41#$41#$41 +
    #$41#$41#$41#$41#$41#$41#$41#$41 +
    #$41#$41#$41#$41#$41#$41#$41#$41 +
    #$41#$41#$41#$41#$41#$00#$00#$21 +
    #$00#$01;

  NB_PORT           = 137;
  NB_BUFSIZE        = 8192;
var
  Buffer            : TIdBytes;
  I                 : Integer;
  RepName           : string;
  UDPClient         : TIdUDPClient;
  msg_prm           : PWMUCommand;
begin
  RepName := '';
  Result := False;
  UDPClient := nil;

  UDPClient := TIdUDPClient.Create(nil);
  try
    try
      with UDPClient do
      begin
        Host := Trim(Data.sAddress);
        Port := NB_PORT;

        Send(NB_REQUEST);
      end;

      SetLength(Buffer, NB_BUFSIZE);
      if (0 < UDPClient.ReceiveBuffer(Buffer, Data.iTimeout)) then
      begin

        for I := 1 to 15 do
          RepName := RepName + Chr(Buffer[56 + I]);

        RepName := Trim(RepName);
        Data.sHostname := RepName;

        Result := True;
      end;

    except
      Result := False;
    end;
  finally
    if Assigned(UDPClient) then
      FreeAndNil(UDPClient);
  end;

  New(msg_prm);
  msg_prm.host := RepName;
  msg_prm.ip := Data.sAddress;
  msg_prm.bOnline := Length(RepName) > 0;

  PostMessage(Data.hMainForm, WM_POSTED_MSG, WM_THRD_SITE_MSG, integer(msg_prm));

end;

procedure TForm2.Button1Click(Sender: TObject);
var
  i                 : integer;
  ArrNetbiosTasks   : array of TNetbiosTask;
  sIp               : string;
begin
  //

  SetMaxAsyncCallThreads(50);

  SetLength(ArrNetbiosTasks, 255);
  sIp := '192.168.1.';
  for i := 1 to 255 do
  begin

    ArrNetbiosTasks[i - 1].hMainForm := Self.Handle;
    ArrNetbiosTasks[i - 1].sAddress := Concat(sIp, IntToStr(i));
    ArrNetbiosTasks[i - 1].iTimeout := 5000;

    AsyncCallEx(@NetBiosLookup, ArrNetbiosTasks[i - 1]);
    Application.ProcessMessages;
  end;
end;

procedure TForm2.ThreadMessage(var Msg: TMessage);
var
  msg_prm           : PWMUCommand;
begin
  //
  case Msg.WParam of
    WM_THRD_SITE_MSG:
      begin
        msg_prm := PWMUCommand(Msg.LParam);
        try
          Memo1.Lines.Add(msg_prm.ip + ' = ' + msg_prm.host + ' --- Online? ' + BoolToStr(msg_prm.bOnline));
        finally
          Dispose(msg_prm);
        end;
      end;
  end;

end;

end.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

撕心裂肺的伤痛 2024-11-08 16:07:36

棘手的事情。我做了一些调试(嗯,相当多的调试),发现 AsyncCallsEx 中的代码在第 1296 行中阻塞:

Result := TAsyncCallArgRecord.Create(Proc, @Arg).ExecuteAsync;

进一步挖掘表明它在 System.pas (_IntfCopy) 中的接口复制中阻塞,参见

CALL    DWORD PTR [EAX] + VMTOFFSET IInterface._Release

Looking at the pascal version of the same code看来这一行释放了先前存储在目标参数中的引用计数。然而,目标是调用者(您的代码)中未使用的结果。

现在是棘手的部分。

AsyncCallEx 返回一个接口(在您的情况下)调用者会丢弃该接口。因此理论上编译后的代码(以伪形式)应该如下所示

loop
  tmp := AsyncCallEx(...)
  tmp._Release
until

但是编译器将其优化为

loop
   tmp := AsyncCallEx(...)
until
tmp._Release

为什么?因为它知道对接口进行赋值会自动释放tmp变量中存储的接口的引用计数(_IntfCopy中对_Release的调用)。所以不需要显式调用_Release。

然而,释放 IAsyncCall 会导致代码等待线程完成。所以基本上你每次调用 AsyncCallEx 时都会等待前一个线程完成......

我不知道如何使用 AsyncCalls 很好地解决这个问题。我尝试了这种方法,但不知怎的,它并没有完全按预期工作(在 ping 大约 50 个地址后程序块)。

type
  TNetbiosTask = record
    //... as before ...
    thread: IAsyncCall;
  end;

  for i := 1 to 255 do
  begin

    ArrNetbiosTasks[i - 1].hMainForm := Self.Handle;
    ArrNetbiosTasks[i - 1].sAddress := Concat(sIp, IntToStr(i));
    ArrNetbiosTasks[i - 1].iTimeout := 5000;

    ArrNetbiosTasks[i - 1].thread := AsyncCallEx(@NetBiosLookup, ArrNetbiosTasks[i - 1]);
    Application.ProcessMessages;
  end;
  for i := 1 to 255 do // wait on all threads
    ArrNetbiosTasks[i - 1].thread := nil;

Tricky stuff. I did some debugging (well, quite some debugging) and found out that the code blocks in AsyncCallsEx in line 1296:

Result := TAsyncCallArgRecord.Create(Proc, @Arg).ExecuteAsync;

Further digging showed that it blocks in interface copy in System.pas (_IntfCopy) at

CALL    DWORD PTR [EAX] + VMTOFFSET IInterface._Release

Looking at the pascal version of the same code it seems that this line release the reference count stored previously in the destination parameter. Destination, however, is a Result which is not used in the caller (your code).

Now comes the tricky part.

AsyncCallEx returns an interface which (in you case) the caller throws away. So in theory the compiled code (in pseudo form) should look like this

loop
  tmp := AsyncCallEx(...)
  tmp._Release
until

However the compiler optimizes this to

loop
   tmp := AsyncCallEx(...)
until
tmp._Release

Why? Because it knows that assigning the interface will release the reference count of the interface stored in the tmp variable automatically (the call to _Release in _IntfCopy). So there's no need to explicitely call _Release.

Releasing the IAsyncCall however causes the code to wait on thread completion. So basically you wait for the previous thread to complete each time you call AsyncCallEx ...

I don't know how to nicely solve this using AsyncCalls. I tried this approach but somehow it is not working completely as expected (program blocks after pinging about 50 addresses).

type
  TNetbiosTask = record
    //... as before ...
    thread: IAsyncCall;
  end;

  for i := 1 to 255 do
  begin

    ArrNetbiosTasks[i - 1].hMainForm := Self.Handle;
    ArrNetbiosTasks[i - 1].sAddress := Concat(sIp, IntToStr(i));
    ArrNetbiosTasks[i - 1].iTimeout := 5000;

    ArrNetbiosTasks[i - 1].thread := AsyncCallEx(@NetBiosLookup, ArrNetbiosTasks[i - 1]);
    Application.ProcessMessages;
  end;
  for i := 1 to 255 do // wait on all threads
    ArrNetbiosTasks[i - 1].thread := nil;
指尖上得阳光 2024-11-08 16:07:36

如果您调用 AsyncCallEx() 或任何其他 AsyncCalls 函数,您将返回一个 IAsyncCall 接口指针。如果其引用计数器达到0,则底层对象将被销毁,这将等待工作线程代码完成。您在循环中调用 AsyncCallEx(),因此每次返回的接口指针都会被分配给同一个(隐藏)变量,从而减少引用计数器,从而同步释放先前的异步调用对象。

要解决此问题,只需将 IAsyncCall 的私有数组添加到表单类中,如下所示:

private
  fASyncCalls: array[byte] of IAsyncCall;

并将返回的接口指针分配给数组元素:

fASyncCalls[i] := AsyncCallEx(@NetBiosLookup, ArrNetbiosTasks[i - 1]);

这将使接口保持活动状态并启用并行执行。

请注意,这只是一般想法,您应该添加代码以在调用返回时重置相应的数组元素,并在释放表单之前等待所有调用完成。

If you call AsyncCallEx() or any other of the AsyncCalls functions you are returned a IAsyncCall interface pointer. If its reference counter reaches 0 the underlying object is destroyed, which will wait for the worker thread code to complete. You are calling AsyncCallEx() in a loop, so each time the returned interface pointer will be assigned to the same (hidden) variable, decrementing the reference counter and thus synchronously freeing the previous asynchronous call object.

To work around this simply add a private array of IAsyncCall to the form class, like so:

private
  fASyncCalls: array[byte] of IAsyncCall;

and assign the returned interface pointers to the array elements:

fASyncCalls[i] := AsyncCallEx(@NetBiosLookup, ArrNetbiosTasks[i - 1]);

This will keep the interfaces alive and enable parallel execution.

Note that this is just the general idea, you should add code to reset the corresponding array element when a call returns, and wait for all calls to complete before you free the form.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文