Wednesday, June 16, 2010

The Future of Delphi

Future of T, that is. Or, in Delphi syntax, TFuture<T>.
Yesterday I wrote about futures in OmniThreadLibrary 2.0 (supported only in D2009+) and I mentioned that implementing futures in plain D2009+ should be really simple. And it really is – it took me all of 15 minutes to write the supporting library and a simple test case.
The code below is released to public domain. I’m claiming no copyrights – use it as you wish. You don’t even have to attribute it to me. Just don’t use it for evil purposes ;)
unit DelphiFuture;

interface

uses
  Classes;

type
  IFuture<T> = interface
    function Value: T;
  end;

  TFutureDelegate<T> = reference to function: T;

  TFutureThread<T> = class(TThread)
  strict private
    FAction: TFutureDelegate<T>;
    FResult: T;
  public
    constructor Create(action: TFutureDelegate<T>);
    procedure Execute; override;
    property Result: T read FResult;
  end;

  TFuture<T> = class(TInterfacedObject, IFuture<T>)
  strict private
    FResult: T;
    FWorker: TFutureThread<T>;
  public
    constructor Create(action: TFutureDelegate<T>);
    function Value: T;
  end;

implementation

uses
  SysUtils;

{ TFutureThread<T> }

constructor TFutureThread<T>.Create(action: TFutureDelegate<T>);
begin
  inherited Create(false);
  FAction := action;
end;

procedure TFutureThread<T>.Execute;
begin
  FResult := FAction();
end;

{ TFuture<T> }

constructor TFuture<T>.Create(action: TFutureDelegate<T>);
begin
  inherited Create;
  FWorker := TFutureThread<T>.Create(action);
end;

function TFuture<T>.Value: T;
begin
  if assigned(FWorker) then begin
    FWorker.WaitFor;
    FResult := FWorker.Result;
    FreeAndNil(FWorker);
  end;
  Result := FResult;
end;

end.
I’ve used my usual test case, calculating number of primes between 1 and 1.000.000.
implementation

uses
  DelphiFuture;

function IsPrime(i: integer): boolean;
var
  j: integer;
begin
  Result := false;
  if i <= 0 then
    Exit;
  for j := 2 to Round(Sqrt(i)) do
    if (i mod j) = 0 then
      Exit;
  Result := true;
end;

procedure TForm1.btnTestClick(Sender: TObject);
var
  numPrimes: IFuture<integer>;
begin
  numPrimes := TFuture<integer>.Create(function: integer
    var
      iPrime: integer;
    begin
      Result := 0;
      for iPrime := 1 to 1000000 do
        if IsPrime(iPrime) then
          Inc(Result);
    end
  );
  lbLog.Items.Add(Format('%d primes from 1 to 1000000',
    [numPrimes.Value]));
end;

13 comments:

  1. Is it not possible/preferable to make TFutureDelegate a reference to a named function. You then create a function called, say CalculatePrime and set up the future thus :

    numPrimes := TFuture.Create(CalculatePrime);

    I just think the use of anonymous methods here can be confusing. btw, I haven't tried this, it's just a thought.

    ReplyDelete
  2. TFuture.Create(function: T) should be possible, true.

    ReplyDelete
  3. Hi Primoz,
    I'm not familiar with the threading terminology of "futures". The only reference via Google that I can find was http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/Future.html. Can you either explain what futures are for us newbies or possibly point us to somewhere on the web for more details. I am particularly interested in knowing under what circumstances would you use futures and what problems do they solve. Thanks in advance and for further extending my curiosity of Delphi threading.

    ReplyDelete
  4. Colin:

    A future is a function that calculates a value that doesn't need to be ready immediately.

    Most of the time, when you call a function, the flow of control transfers to it until the function returns. You could look at it as if the function call blocks the current procedure until it's finished.

    A future is basically a non-blocking function call. You create it and it starts to run in a separate thread, while you do other things in the current function. Then later on, you ask for the value of the future, which it's been calculating all this time. If it's not done, it will block at that point until it's finished, otherwise it gives you the value immediately.

    For example, if you need to calculate two different values and add them together, and they're both rather involved calculations, you can put one of them in a future and have it run in a different thread and on a different processor while you're calculating the other one. Then, instead of taking X + Y amount of time to do the whole calculation, it takes X or Y, whichever is longer. If X and Y are close to equal, then you've cut your processing time in half.

    ReplyDelete
  5. Hi Mason,

    Thanks for the explanation! I owe quite a bit of my understanding of all things new (and sometimes old) in Delphi to your replies in stackoverflow. I'm re-reading Primoz's posts and now it's beginning to make a bit of sense now (Not because it was poorly worded but because of my previous lack of understanding!).

    Now if I can just get my head around the rest of the OTL... :)

    ReplyDelete
  6. @Mason: Thanks for the detailed answer.

    @Colin: More info on futures in Task Parallel Library: http://www.devx.com/dotnet/Article/39204/1763/page/5. There's also a longer description at Wikipedia: http://en.wikipedia.org/wiki/Futures_and_promises.

    The implementation above is just a proof of concept and doesn't implement advanced features from the Java implementation. The OTL version will have more functionality.

    ReplyDelete
  7. BTW, if anybody can find a Future definition on the MSDN, can she/he please post the link here? If I try to search for "Future", Microsoft's searcher just barfs a page of errors back to me :(

    ReplyDelete
  8. Thanks Primoz!
    A little (and dirty) improvement:

    type
    IFuture = interface
    function Value: T;
    function Available: boolean; //added method
    end;

    //implementation
    function TFuture.Available: boolean;
    begin
    Result := WaitForSingleObject(FWorker.Handle, 1) <> WAIT_TIMEOUT; //could be more robust
    end;


    so you can check the availability of the result

    if not numPrimes1.Available then
    begin
    ListBox1.Items.Add('Still not available');
    Exit;
    end
    else
    ListBox1.Items.Add(Format('%d primes from 1 to 1000000',
    [numPrimes1.Value]));

    ReplyDelete
  9. Something similar will appear in the OTL version: Cancel, IsCanceled and IsDone.

    ReplyDelete
  10. @Babnik: It seems that function and methods are automatically upgraded to delegates. (You can pass a "function: integer" or "function: integer of object" to a method that expects "reference to function: integer" and everything will work correctly.) So there's no need to enhance the base class, it should already accept such functions/methods. (I've tested this with the OTL version and everything works just fine.)

    BTW, does anybody know if this behaviour is documented?

    ReplyDelete
  11. Also missing from the code above:

    destructor TFuture.Destroy;
    begin
    FreeAndNil(FWorker);
    inherited;
    end;

    Without that, the code will leak if a future is released before the Value function is called.

    ReplyDelete
  12. @gabr - http://docwiki.embarcadero.com/RADStudio/en/Anonymous_Methods_in_Delphi explicitly says methods are directly assignable to 'reference to' type variables. While only on blogs, Barry Kelly has also said in the past that assignment compatibility with both method and standalone routines was intentional (e.g. see his comment at http://sergworks.wordpress.com/2010/01/27/anonimous-methods-in-delphi-the-internals/#comment-5).

    ReplyDelete
  13. @Chris: Thanks for the links!

    ReplyDelete