【问题标题】:Is it possible to create a thread pool using AsyncCalls unit?是否可以使用 AsyncCalls 单元创建线程池?
【发布时间】:2011-04-15 01:45:43
【问题描述】:

我正在尝试使用AsyncCalls 在整个 C 类子网上执行 Netbios 查找。理想情况下,我希望它同时执行 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.

【问题讨论】:

  • 我在这里没有看到任何错误。你确定它不起作用吗?
  • 代码在一个新线程中执行,但它会一个接一个地执行一个请求,而不是同时执行多个请求。理想情况下,我希望它产生 10-50 个工作线程并让这些工作线程完成所有任务,直到没有剩余。

标签: multithreading delphi


【解决方案1】:

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

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

进一步挖掘表明它在 System.pas (_IntfCopy) 中的接口副本中阻塞

CALL    DWORD PTR [EAX] + VMTOFFSET IInterface._Release

查看相同代码的 pascal 版本,这一行似乎释放了先前存储在目标参数中的引用计数。然而,Destination 是调用者(您的代码)中未使用的 Result。

现在是棘手的部分。

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;

【讨论】:

  • 感谢您的详细解答。看来 AsyncCalls 不是此任务的正确工具。
【解决方案2】:

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

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

private
  fASyncCalls: array[byte] of IAsyncCall;

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

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

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

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

【讨论】:

    猜你喜欢
    • 2015-03-28
    • 2012-06-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-04
    • 1970-01-01
    • 1970-01-01
    • 2017-11-03
    相关资源
    最近更新 更多