Wednesday, October 15, 2008

Value Capture vs. Variable Capture

Anonymous methods (aka. Closures) capture the variables from the outer scope that are referenced within the body of the method itself. This is the intended behavior as it serves to extend the lifetime of the variable to match that of the anonymous method itself. This should not be confused with capturing the value of a variable. In my previous posts, A Sink Programming and More A Sink Kronos Programming, I demonstrated a technique for asynchronously dispatching an anonymous method from a background thread into the main UI thread. I also mentioned how there was a potential race-condition on the SR local variable. The simple, yet not very scalable, way of eliminating this race was to pause the loop by calling EndInvoke() prior to accessing SR again (in the FindNext() call). This time, I'm going to show a technique for capturing the value of the SR.Name field and not the whole SR variable. This will eliminate the race on the SR variable because the anonymous method body will no longer need to access it.

Value capturing is a little more manual, but through the use of generic methods and a corresponding generic class, we only need to create this code once. The idea is to add an overloaded BeginInvoke() method that takes an extra parameter, which will be the value we want to capture and pass along to the anonymous method. Here's the new BeginInvoke method:


TControlHelper = class helper for TControl
...
function BeginInvoke<T1>(const AProc: TProc<T1>; const Param: T1): IASyncResult; overload;
...
end;

This overload is different from the BeginInvoke<TResult> version since the AProc parameter takes a procedure reference instead of a function reference. As you can see, the procedure reference (can be an anonymous method), AProc, is defined as taking a single parameter of type T1. There is also an extra parameter which is where we'll pass in the value we want to capture. Behind the scenes we'll need to save off this value some place so when the procedure is called, that value can be passed along. For this, we'll create another descendant of the TBaseAsyncResult class, only this time it is a generic class because we need to have a field of type T1:

  TAsyncProcedureResult<T1> = class sealed (TBaseAsyncResult)
private
FAsyncProcedure: TProc<T1>;
FParam: T1;
protected
procedure AsyncDispatch; override;
constructor Create(const AAsyncProcedure: TProc<T1>; const Param: T1);
end;

As before, we override the AsyncDispatch abstract virtual method and add a constructor that takes the procedure reference and the value. The body of BeginInvoke<T1>() looks like this:

function TControlHelper.BeginInvoke<T1>(const AProc: TProc<T1>; const Param: T1): IASyncResult;
begin
Result := TAsyncProcedureResult<T1>.Create(AProc, Param).Invoke;
end;

Now let's change the background thread code to take advantage of this:

procedure TBeginInvokeTestForm.TSearchThread.Execute;
var
SR: TSearchRec;
SH: Integer;
AR: IAsyncResult;
begin
if not Terminated then
begin
AR := FForm.ListBox1.BeginInvoke<string>(TFunc<string>(function: string
begin
Result := FForm.Edit1.Text;
end));
FFolder := FForm.ListBox1.EndInvoke<string>(AR);
SH := FindFirst(IncludeTrailingPathDelimiter(FFolder) + '*.*', faAnyFile, SR);
while (SH = 0) and not Terminated do
begin
//Sleep(10); // this makes the background thread go a little slower.
AR := FForm.ListBox1.BeginInvoke<string>(TProc<string>(procedure (SRName: string)
begin
if not Terminated then
FForm.ListBox1.Items.Add(SRName);
end),
SR.Name); // Pass the value of SR.Name on through.
// FForm.ListBox1.EndInvoke(AR); { this call can be safely removed since SR isn't
touched inside the anonymous method body}
SH := FindNext(SR);
end;
end;
end;

Now the the loop in this thread will execute as quickly as it can to dispatch asynchronous calls to the main UI thread regardless of how fast they can be consumed. This same technique can be applied to the "TFunc" version by adding more BeginInvoke() overloads. It can also be extended to allow more than one extra parameter so you can capture and pass long many values. In the above case, it only specifically captured just the string that is the name of the file. It could have also captured the value of the whole SR structure.


What about exceptions? What if an exception were raised while the anonymous method were executing? It would still get caught and propagated back to the specific IAsyncResult instance but because EndInvoke() isn't being called it is lost in the ether. It is also "bad form" to forgo the call to EndInvoke() as there may be some other internal cleanup that needs to happen. A simple way to deal with this is to store the IAsyncResult instances in a local TList<IAsyncResult> list. Then once the loop is done, iterate through the list, calling EndInvoke() on each one. This may still be less than ideal because you cannot cancel subsequent invocations of the anonymous method which may just stack up a whole plethora of exceptions. In this instance, aside from the exception problem, it is ok to defer or not call EndInvoke(). In other cases this may not be true, such as using this technique for overlapped IO.

2 comments:

  1. Just wanted to drop an comment and tell you that I really enjoy reading posts like this!
    Looking forward to read about overlapped IO.

    ReplyDelete
  2. [...] http://blogs.embarcadero.com/abauer/2008/10/15/38876 [...]

    ReplyDelete

Please keep your comments related to the post on which you are commenting. No spam, personal attacks, or general nastiness. I will be watching and will delete comments I find irrelevant, offensive and unnecessary.