Lets take the following fictional example.
There is form with a single button and a list box. When the button is pressed a long process occurs, when the process is complete it needs to add an item to the list box.
This could be done without Multi-threading.
procedure TForm5.Button1Click(Sender: TObject); begin Button1.Enabled := False; SlowProc; Button1.Enabled := True; end; procedure TForm5.SlowProc; begin Sleep(10000); // simulate long process Listbox1.Items.Add('10 Seconds'); end;
During testing it is determined that the user needs to be able to re-size the application while the long process is running.
But never fail XE7 has been released with TPL and TTask to the rescue.
The first iteration SlowProc is changed to use a task.
procedure TForm5.SlowProc; var Task : ITask; begin Task := TTask.Create( procedure begin Sleep(10000); // simulate long process Listbox1.Items.Add('10 Seconds'); end); Task.Start; end;
Run the application and it appears to work. Then further testing reveals a couple of problems.
The first being that button can now be pressed multiple times. The second is that if the form is closed form right after pressing the button an few seconds later and access violation occurs.
- The reason the button can be pressed multiple times is that the enabled is set back to true after the task is started and not
- The cause of the access violation is that the code is still executing after the form has been freed.
The second iteration the code is now changed:
procedure TForm5.Button1Click(Sender: TObject); begin Button1.Enabled := False; SlowProc; end; procedure TForm5.SlowProc; var Task : ITask; begin Task := TTask.Create( procedure begin Sleep(10000); if Assigned(ListBox1) then begin Listbox1.Items.Add('10 Seconds'); Button1.Enabled := True; end; end); Task.Start; end;
This now appears to work. But, now there can be an up to 10 second delay before the application stops running after the form closes. Now this is fictional example that has just a single sleep() call. This can occur with real world items as well, but often there are several steps in the method, so I am going to simulate multiple steps, with a loop 0..9 with a call to sleep(1000);
procedure TForm5.SlowProc; var Task : ITask; begin Task := TTask.Create( procedure var I : Integer; begin for I := 0 to 9 do Sleep(1000); if Assigned(ListBox1) then begin Listbox1.Items.Add('10 Seconds'); Button1.Enabled := True; end; end); Task.Start; end;
Now the fictional example show multiple steps. But it does not solve the problem with the application running for up to 10 seconds after the main form is closed. When the form is begin closed the Task needs to be notified so it can stop running. This can be be done with the ITask.Cancel method.
To resolve this a third iteration is produced.
Task : ITask; has been moved from SlowProc, and is now a member of the form.
procedure TForm5.Button1Click(Sender: TObject); begin Button1.Enabled := False; SlowProc; end; procedure TForm5.FormDestroy(Sender: TObject); begin Task.Cancel; end; procedure TForm5.SlowProc; begin Task := TTask.Create( procedure var I : Integer; begin for I := 0 to 9 do begin if TTask.CurrentTask.Status = TTaskStatus.Canceled then exit; Sleep(1000); end; if Assigned(ListBox1) then begin Listbox1.Items.Add('10 Seconds'); Button1.Enabled := True; end; end); Task.Start; end;
This all appears to work and is released. Sometime later in real world strange behaviors and errors are reported on this screen. After research it is learned that the GUI is not thread safe, so we use a TThread.Queue, to the GUI code to run in the main thread.
Now onto the forth iterations of the code
procedure TForm5.SlowProc; begin Task := TTask.Create( procedure var I : Integer; begin for I := 0 to 9 do begin if TTask.CurrentTask.Status = TTaskStatus.Canceled then exit; Sleep(1000); end; if TTask.CurrentTask.Status <> TTaskStatus.Canceled then begin TThread.Queue(TThread.CurrentThread, procedure begin if Assigned(ListBox1) then begin Listbox1.Items.Add('10 Seconds'); Button1.Enabled := True; end; end); end; end); Task.Start; end;
Now we have an finally application that should work without error. Granted this a fictional example, but it shows just some of the pitfalls that can come with multi-threading. Each is relatively easy to deal with.
Great example! Why not just call Synchronize(...) instead of TThread.Queue(...) ?
ReplyDeleteI have just seen it done that way and never thought to ask that question.
DeleteAfter doing a code review, it appears there is a slightly different behavior in both. Ultimately both call TThread.Synchronize(ASyncRec: PSynchronizeRecord; QueueEvent: Boolean = False)
QueueEvent = true when using Queue, otherwise the default is false which is what the other typical synchronize method use.
I am not 100% sure but it appears the Synchronize will pause the current thread waiting a INFINITE amount of time for the code to execute. Queue will place it in the queue to be run and not then return.
Synchronize tries to post it to the main thread immediately and runs greater risk of deadlock. Queue waits for the main thread to be idle and has less risk of deadlock.
DeleteAgreed, there is no need to wait at the end of the task to update form controls. Queue seems the right thing for the task here.
DeleteNow your button will stay disabled if your task is being canceled somehow.
ReplyDeleteVery true, although currently the only way to cancel task is to free the form.
DeleteDang! This is the third article I've seen about Task Parallel Library, it looks very nice. Almost single handedly, it makes me consider upgrading my codebase from XE2.
ReplyDeleteIMHO
ReplyDeleteTThread.Queue(TThread.CurrentThread,
should be
TThread.Queue(nil, //synchronize to the main thread
The documentation http://docwiki.embarcadero.com/Libraries/XE7/en/System.Classes.TThread.Queue
DeleteMentions that: "You can use nil/NULL as the AThread parameter if you do not need to know the information of the caller thread in the main thread."
Given this example, where I don't need to reference the original thread, it's not required.
The new PPL makes Parallel programming easier, but you still need understand how it works and handle threads appropriately. Good post.
ReplyDeleteUse messages to communicate between threads and components (good old PostMessage and message procedures). Object reference are problematic, either because the objects can be freed, or in a GC/ARC because they will retain references.
ReplyDeleteA message will just be discarded automatically if its target is gone.
As a further step, you also need tho provision against exceptions, and give an ability to cancel a task before it has been completed.
In the end, good tasks are either complex hand-made, case-specific tailored affairs, or ... scripts! A good script engine will allow to cancel any task at any time, will wrap exceptions and will naturally keep script-side references distinct from those Delphi-side through sandboxing.