Jump to content
David Schwartz

Parallel processing question

Recommended Posts

I have a class that contains some logic that's applied to data selected by the user. The selections are added to a TListView, and an instance of the object is attached to the .Data property for each ListItem.

 

They're being processed more or less in sequence right now, and I believe the overall processing time can be sped up a lot if I process each one as a task. 

 

I want to update columns in the ListView as each one finishes with a status message and elapsed time.

 

But using the PPL, there's nothing that says when a task has completed.

 

So I'm curous ... what's the best way to get an OnComplete event from each task to update the ListView and also figure out when everything has finished successfully?

Edited by David Schwartz

Share this post


Link to post

What comes to mind is a client server model using batches (along the lines of database data entry).

IOW, let the end user decide when to process the batch/ update.

Share this post


Link to post

I don't think you're understanding the question. 

 

When you do most DB operations, there's an OnxxxCompleted event that's called.

 

With tasks, there's ... nothing. They just stop.

 

When each task finishes, I want to update the line in the ListView that says it's Finished and show the Elapsed Time. 

 

Not when the entire batch is finished. When each one is finished.

 

WaitForAny just says at least one of them has finished, not which one. WaitForAll says they've ALL finished.

 

How do I know when each one finishes, without polling their current state?

Share this post


Link to post
2 minutes ago, David Schwartz said:

I don't think you're understanding the question. 

OK, sorry David.

Just showing my ignorance again 😢.

Share this post


Link to post

I'm looking more closely at the Omni Thread Library ... it seems to have more to offer than the what's in Delphi. Just no documentation or help files.

Edited by David Schwartz

Share this post


Link to post
37 minutes ago, David Schwartz said:

When you do most DB operations, there's an OnxxxCompleted event that's called.

 

With tasks, there's ... nothing. They just stop.

That is true, there is no OnCompleted event

 

37 minutes ago, David Schwartz said:

When each task finishes, I want to update the line in the ListView that says it's Finished and show the Elapsed Time. 

 

You can do that inside the task code. You can either send message with some messaging system or call appropriate code directly (make sure it is synchronized with the main thread for UI interaction). To free thread for further processing it is better to use TTask.Queue instead of TThread.Synchronize, but if you need to perform some cleanup, Synchronize will do as long as it is not too slow. Something like:

 

TTask.Run(
  procedure
  begin
    // your background work
    ...
    // task is completed
    TThread.Queue(nil,
      procedure
      begin
        // update the UI
      end);
  end);

 

Share this post


Link to post

I prefer how Omni Thread Library does it:

 

  Go_btn.Enabled := False;
  Parallel.Async(
    procedure
    begin
      // executed in background thread
      Sleep(1000);
    end,
    Parallel.TaskConfig.OnTerminated(
      procedure (const task: IOmniTaskControl)
      begin
        // executed in main thread
        ShowMessage( 'done!' );
        Go_btn.Enabled := True;
      end
    )
  );

Right now, this bit of code is attached to a button.OnClick hander for testing.

 

If this were inside of a class, I don't think the logic for updating the ListView belongs there.

 

Which means that the OnTerminated proc there needs to trigger a common event handler, passing in an ID for which line / ListItem that thread is processing and perhaps a status indicator. Which is pretty much the same as using the queue you suggested.


Or ... this whole bit of code is in a form method that's called and passed each ListItem where the .Data property has an object to be processed, and it has a method in it that the Async method runs as a task. Then the OnTerminated part updates the ListView.

 

Which approach would you go with?

 

Share this post


Link to post
4 minutes ago, David Schwartz said:

If this were inside of a class, I don't think the logic for updating the ListView belongs there.

 

Which means that the OnTerminated proc there needs to trigger a common event handler, passing in an ID for which line / ListItem that thread is processing and perhaps a status indicator. Which is pretty much the same as using the queue you suggested.


Or ... this whole bit of code is in a form method that's called and passed each ListItem where the .Data property has an object to be processed, and it has a method in it that the Async method runs as a task. Then the OnTerminated part updates the ListView.

 

Which approach would you go with?

You are right that the processing logic and updating UI don't belong together. 

 

When I have processing functionality as part of the class (and I usually do) I also declare completion handlers in that class. And then you can easily attach any kind of additional logic when processing is finished - including updating UI. The most important part is that all individual pieces of code can be easily unit tested (except for UI) and that there is minimal gluing code where you can make mistakes when you put all of the code together.

Share this post


Link to post
1 hour ago, Dalija Prasnikar said:

You are right that the processing logic and updating UI don't belong together. 

 

When I have processing functionality as part of the class (and I usually do) I also declare completion handlers in that class. And then you can easily attach any kind of additional logic when processing is finished - including updating UI. The most important part is that all individual pieces of code can be easily unit tested (except for UI) and that there is minimal gluing code where you can make mistakes when you put all of the code together.

But if class instances are attached to the .Data properties of the ListItems, then they'd need to know which LI they're attached to in order to update them, right?

 

I'm thinking something like this to kick them all off:

 

  for var li in lview1.Items do
  begin
  
    TMyClass( li.Data ).Execute( li ); 
    
  end;

 

Would you create the task  inside of the .Execute method?

 

Or around that line? (suggesting that the 'li' parameter would probably not need to be injected)

 

Would it make much of a difference? 

 

EDIT: actually, Omni Thread Library has a ForEach that looks like it can take the .Items directly and process them all as separate threads using the same Proc.

 

Edited by David Schwartz

Share this post


Link to post
37 minutes ago, David Schwartz said:

But if class instances are attached to the .Data properties of the ListItems, then they'd need to know which LI they're attached to in order to update them, right?

 

I'm thinking something like this to kick them all off:

 


  for var li in lview1.Items do
  begin
  
    TMyClass( li.Data ).Execute( li ); 
    
  end;

 

Yes, you can use that approach, but it is not flexible because your processing function depends on the specific UI type.

 

37 minutes ago, David Schwartz said:

Would you create the task  inside of the .Execute method?

 

Or around that line? (suggesting that the 'li' parameter would probably not need to be injected)

 

Would it make much of a difference? 

 

 

I wouldn't create task inside Execute method because processing functionality itself should be implemented as clean synchronous code - that gives you more flexibility to combine its functionality as needed.

 

I would use something like:

 

  TMyClass = class
  public
    procedure Execute(OnCompleted: TProc);
  end;


procedure TMyClass.Execute(OnCompleted: TProc);
begin
  // process data
  //...
  if Assigned(OnCompleted) then
    OnCompleted;
end;

procedure TMyForm.ProcessItem(li: TListViewItem);
begin
  TTask.Run(
    procedure
    begin
      TMyClass(li.Data).Execute(
        procedure
        begin
          TThread.Queue(nil,
          procedure
          begin
            UpdateItem(li);
          end);
        end);
    end);
end;

procedure TMyForm.UpdateItem(li: TListViewItem);
begin
  li. ...
end;

procedure TMyForm.ProcessView;
begin
  for var li in lview1.Items do
  begin
    ProcessItem(li);
  end;
end;

 

 

  • Like 2

Share this post


Link to post

If you use TTask.Run(), it returns an ITask which resembles the 'future' pattern - the interface provides a getStatus() to check if it is complete or not, as well as other helper methods. 

From OmniThreadLibrary does seem like the Parallel.Async() could be extended to return something similar.

 

However, using ITask / IFuture requires 'polling' type behavior compared to callbacks ... but I think what Dalija has just suggested is closer to what you probably want.

Edited by darnocian

Share this post


Link to post
28 minutes ago, Dalija Prasnikar said:

I wouldn't create task inside Execute method because processing functionality itself should be implemented as clean synchronous code - that gives you more flexibility to combine its functionality as needed.

 

I would use something like:

Perfect. Thanks!

Share this post


Link to post
On 12/25/2021 at 1:50 PM, Dalija Prasnikar said:

I would use something like:

I'd additionally untie processing class from UI listview item completely. Processor just doesn't need to know about UI (unless it deals with it directly, like bg rendering or similar - but here it's not the case I suppose).

Share this post


Link to post
14 minutes ago, Fr0sT.Brutal said:

I'd additionally untie processing class from UI listview item completely. Processor just doesn't need to know about UI (unless it deals with it directly, like bg rendering or similar - but here it's not the case I suppose).

But it is completely decoupled in that code. The only connection is in attached completion handler that can hold any kind of code and is not part of processing class.

Share this post


Link to post
7 minutes ago, Dalija Prasnikar said:

But it is completely decoupled in that code. The only connection is in attached completion handler that can hold any kind of code and is not part of processing class.

Yep you're right. However, there's too much of JS-ism.

Share this post


Link to post
22 minutes ago, Fr0sT.Brutal said:

Yep you're right. However, there's too much of JS-ism.

Instead of anonymous methods, you can also use plain TNotifyEvent and pass list item as Sender. 

 

Also if you don't like the code in completion handler because of TThread.Queue, this call can be moved to UpdateItem. There are many ways to write this functionality, this is just one. 

Share this post


Link to post

I was able to do this with Omni Thread Library's Async method without too much trouble.

 

I set up 20 dummy tasks that each took 2-14 seconds to run (randomly assigned). 

 

The entire batch took 14.8 seconds to run them all in parallel. 

 

THAT is what I was looking to accomplish!

 

(For my application, I'll have 25-100 tasks, and most of them will be sitting waiting. If there's a problem, the timeout is 30 secs, but that rarely happens. The average I've seen is in the 10-15 second range. So if they can all get processed within 15-20 seconds, that'll be awesome.)

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×