Limitation
You really, really shouldn't keep references to threads that are set to free on termination. It is asking for all sorts of problems. The code below does use threads with FreeOnTerminate set to True. This is only safe because: 1) the references are removed as the thread terminates and before it is freed; 2) The OnTerminate handler is called in the context of the mainthread and/or a thread safe TList descendant is used; and - most importantly - 3) the references are only used to cancel threads selectively from within the context of the main thread.
As soon as you want to use the references for anything else (like pausing and resuming) or could access them from within the context of any thread other than the main thread, you really should delve a lot deeper into various mechanisms of threaded program execution.
A couple of keywords: thread synchronization, event signalling, mutex's, semaphores, critical sections.
A good reference to multi-threaded programming with Delphi is Multithreading - The Delphi Way
Answer
If you really need to reference the threads later on, you do need to store your references somewhere. But you do not need to have separate variables for each thread you create. There are many options. An array comes to mind, but probably the simplest is an object list (from the Contnrs
unit):
TForm1 = class(TForm)
private
MyThreads: TObjectList;
constructor TForm1.Create(AOwner: TComponent);
begin
inherited;
MyThreads := TObjectList.Create;
end;
destructor TForm1.Destroy;
begin
MyThreads.Free;
inherited;
end;
TObjectList has an OwnsObjects
property which by default is True
. This means that freeing the object list will also free the instances it contains. And this means that you need to ensure that the list only contains valid references. Normally that is not a problem, but with threads you need to take special care, especially when you use FreeOnTerminate
set to True
.
Create your threads like you do in your first example, with a couple of changes:
Use a local variable (you should avoid global variables, even if they are only visible within the unit as much as possible).
Add the thread to your object list.
Set an OnTerminate handler to remove the instance from your object list when you also want to use 'FreeOnTerminate` set to True.
procedure TForm1.Button1Click(Sender: TObject);
var
Thread: TThread; // Local var
begin
Thread := TMyThread.Create(True);
MyThreads.Add(Thread); // Add to list
Thread.source_file :='http://example.com/download1.zip';
Thread.destination_file := 'c:\download1.zip';
Thread.download_item_id := 0;
Thread.Priority := tpNormal;
Thread.OnTerminate := HandleThreadTerminate; // Ensure you keep list valid
Thread.FreeOnTerminate := True; // Advice against this!
Thread.Start;
end;
procedure TForm1.HandleThreadTerminate(Sender: TObject);
var
idx: Integer;
begin
// Acquire Lock to protect list access from two threads
idx := MyThreads.IndexOf(Sender);
if idx > -1 then
MyThreads.Delete(idx);
// Release lock
end;
The HandleThreadTerminate procedure should use some kind of lock, so two threads cannot at the same time try to delete instances from the list as a thread switch between IndexOf and Delete could mean deleting the wrong instance. The code in the handler could be written as MyThreads.Remove(Sender)
. Although that is a single statement, it isn't thread safe as it does the same as the code I've shown behind the scenes.
Edit: Actually, as @LURD mentions it happens to be thread safe because OnTerminate is called in the context of the main thread. I am leaving the TThreadList example because it is a good way to do things if you need / happen to access the list from multiple threads.
To have the locking stuff taken care of automatically, you can use a TThreadList (from the classes
unit). Required code changes:
Of course you need to instantiate a TThreadList instead of a TObjectList.
constructor TForm1.Create(AOwner: TComponent);
begin
inherited;
MyThreads := TThreadList.Create;
end;
Because TThreadList has no concept of OwnsObjects
, you will have to free any remaining threads yourself:
destructor TForm1.Destroy;
var
LockedList: TList;
idx: integer;
begin
LockedList := MyThreads.LockList;
try
for idx := LockedList.Count - 1 downto 0 do
TObject(MyThreads.Items(idx)).Free;
finally
MyThreads.UnlockList;
end;
MyThreads.Free;
inherited;
end;
The OnTerminate handler can now safely be simplified as the TThreadList will take care of the required locking.
procedure TForm1.HandleThreadTerminate(Sender: TObject);
begin
MyThreads.Remove(idx);
end;
Edit: As @mghie mentions, freeing a thread when FreeOnTerminate is true clashes with the whole idea of free on terminate. You can either set FreeOnTerminate to false before you free the thread (which would terminate it), or if you want to keep the FreeOnTerminate handling, you should disconnect the OnTerminate handler and then use Terminate to tell the thread to terminate.
destructor TForm1.Destroy;
var
LockedList: TList;
idx: integer;
begin
LockedList := MyThreads.LockList;
try
for idx := LockedList.Count - 1 downto 0 do
begin
TThread(MyThreads.Items(idx)).OnTerminate := nil;
TThread(MyThreads.Items(idx)).Terminate;
end;
finally
MyThreads.UnlockList;
end;
MyThreads.Free;
inherited;
end;