Tricks. I did some debugging (well, pretty some debugging) and found out that the code blocks in AsyncCallsEx are on line 1296:
Result := TAsyncCallArgRecord.Create(Proc, @Arg).ExecuteAsync;
Further digging showed that it blocks copying the interface in System.pas (_IntfCopy) in
CALL DWORD PTR [EAX] + VMTOFFSET IInterface._Release
Looking at the pascal version of the same code, it seems that this line frees up the reference count stored earlier in the target parameter. The assignment, however, is a result that is not used by the caller (your code).
Now comes the hard part.
AsyncCallEx returns the interface that (in your case) the caller throws. Therefore, a theoretically compiled code (in pseudo form) should look like this:
loop tmp := AsyncCallEx(...) tmp._Release until
However, the compiler optimizes this for
loop tmp := AsyncCallEx(...) until tmp._Release
Why? Because he knows that assigning an interface will automatically release the reference count of the interface stored in the tmp variable (calling _Release in _IntfCopy). Therefore, there is no need to explicitly call _Release.
Releasing IAsyncCall causes the code to wait for the thread to complete. So basically you wait until the previous thread completes every time you call AsyncCallEx ...
I do not know how to beautifully solve this problem with AsyncCalls. I tried this approach, but somehow it does not work as expected (program blocks after ping 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;