David Schwartz 426 Posted December 25, 2021 (edited) 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 December 25, 2021 by David Schwartz Share this post Link to post
angusj 126 Posted December 25, 2021 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
David Schwartz 426 Posted December 25, 2021 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
angusj 126 Posted December 25, 2021 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
David Schwartz 426 Posted December 25, 2021 Well, thanks for the effort. 🙂 Share this post Link to post
David Schwartz 426 Posted December 25, 2021 (edited) 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 December 25, 2021 by David Schwartz Share this post Link to post
Dalija Prasnikar 1396 Posted December 25, 2021 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
David Schwartz 426 Posted December 25, 2021 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
Dalija Prasnikar 1396 Posted December 25, 2021 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
David Schwartz 426 Posted December 25, 2021 (edited) 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 December 25, 2021 by David Schwartz Share this post Link to post
Dalija Prasnikar 1396 Posted December 25, 2021 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; 2 Share this post Link to post
darnocian 84 Posted December 25, 2021 (edited) 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 December 25, 2021 by darnocian Share this post Link to post
David Schwartz 426 Posted December 25, 2021 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
Fr0sT.Brutal 900 Posted December 27, 2021 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
Dalija Prasnikar 1396 Posted December 27, 2021 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
Fr0sT.Brutal 900 Posted December 27, 2021 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
Dalija Prasnikar 1396 Posted December 27, 2021 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
David Schwartz 426 Posted December 28, 2021 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