7

First of all, I'm still familiarizing myself with multi-threading, and don't know much terminology. I need to make sure I'm doing this right, because it's a sensitive subject.

Specifications

What I'm building is a component which will contain a dynamic number of threads. Each of these threads is re-used for performing a number of requests. I can provide all necessary details to the thread as I create it and before I execute it, as well as provide event handlers. Once it's executed, I'm pretty much done with one request, and I feed in another request. The requests are being fed into these threads from another stand-alone background thread which is constantly processing a queue of requests. So this system has two lists: 1) List of request records, and 2) List of thread pointers.

I'm using descendants of the TThread class (at least this is the threading method I'm familiar with). I'm getting feedback from the threads by synchronizing event triggers which I assigned when the threads were created. The threads are loading and saving data in the background, and when they're done, they reset themselves ready to process the next request.

Problem

Now the trouble begins when deciding how to handle the event of changing the number of allowed threads (via a property of the component ActiveThreads: TActiveThreadRange which TActiveThreadRange = 1..20). Therefore, there can be anywhere between 1 and 20 threads created at a time. But when, let's say, the application using this component changes this property from 5 to 3. At this time, there are already 5 threads created, and I don't want to forcefully free that thread if it happens to be busy. I need to wait until it's done before I free it. And on the other hand, if the property is changed from 3 to 5, then I need to create 2 new threads. I need to know the proper approach to 'keep track' of these threads in this scenario.

Possibilities

Here are some possible ways I can think of to 'track' these threads...

  • Keep a TList containing each created thread - easy to manage
  • Create a TList wrapper or descendant containing each created thread - easier to manage, but more work
  • Keep an array containing each created thread - Would this be better than a TList?
  • Create an array wrapper containing each created thread

But then back to my original issue - What to do with existing busy threads when the ActiveThreads property is decreased? Creating them is no problem, but releasing them is becoming confusing. I usually make threads which free themselves, but this is the first time I've made one which is re-used. I just need to know the proper method of destroying these threads without interrupting their tasks.

Update

Based on the feedback, I have acquired and begun implementing the OmniThreadLibrary (as well as the long needed FastMM). I've also changed my approach a little - A way that I can create these threaded processes without managing them and without another thread to process the queue...

  • 1 master method to spawn a new process
    • function NewProcess(const Request: TProcessRequest): TProcessInfo;
    • TProcessRequest is a record with specifications of what's to be done (Filename, Options, etc.)
    • TProcessInfo is a record which passes back some status information.
  • Feed in an event handler for the event of being 'done' with its task when creating a new process. When component receives this message, it will check the queue.
    • If command is queued, it will compare the active process limit with current process count
    • > If exceeds limit, just stop and next completed process will do perform same check
    • > If within limit, kick off another new process (after ensuring previous process is done)
    • If no commands are queued, then just stop
  • Each process can die on its own after it has done its task (no keep-alive threading)
  • I won't have to worry about another timer or thread to continually loop through
    • Instead each process destroys its self and checks for new requests before doing so

Another Update

I have actually reverted back to using a TThread, as the OTL is very uncomfortable to use. I like to keep things wrapped and organized in its own class.

Jerry Dodge
  • 26,858
  • 31
  • 155
  • 327
  • What you're asking about is known as a "thread pool"; perhaps that will help you find resources. – Mike W Jan 26 '12 at 20:50
  • 5
    See [delphi-threaded-list-of-thread-jobs-queueing](http://stackoverflow.com/questions/1805633/delphi-threaded-list-of-thread-jobs-queueing). And do not roll your own thread pool, look at [OTL-OmniThreadLibrary](http://code.google.com/p/omnithreadlibrary/). – LU RD Jan 26 '12 at 20:58
  • My personal experience is that it is easier to work with the Windows API directly rather than to rely on `TThread` if you are doing some more sophisticated work. In fact, it is *very* easy to get started to threading using the Windows API. Start by doing simple experiments with [`CreateThread`](http://msdn.microsoft.com/en-us/library/windows/desktop/ms682453(v=vs.85).aspx). Just beware that you, as a Delphi developer, probably should use the `System.BeginThread` wrapper instead of `CreateThread`, but, of course, the `CreateThread` MSDN documentation is still valid. – Andreas Rejbrand Jan 26 '12 at 20:58
  • 6
    I disagree with @Andreas here. I think that is bad advice, especially for Jerry. What Jerry needs here is a high level abstraction and a good lib like OTL or AsyncCall. He also needs to read a good primer on threading. For example Joe Duffy's book: http://bluebytesoftware.com/books/winconc/winconc_book_resources.html The reason I feel that you need to read up on the basics is that what I have seen of your previous efforts included no synchronisation at all. No waiting. No serialisation. And a `Sleep(10)` call to avoid a busy loop. You need to get on top of the basics. – David Heffernan Jan 26 '12 at 21:06
  • A thread pool would get the job done here but your specific problem is probably more suited to a pipeline approach. Perhaps a two stage pipeline. Stage 1 to read the file into memory. Stage 2 to decode and process the image. Stage 2 could then save it, or maybe it would be preferred to pass that onto stage 3. – David Heffernan Jan 26 '12 at 21:08
  • 7
    I just wonder, why so many down-votes? – kobik Jan 26 '12 at 21:14
  • 3
    I like [Multithreading - The Delphi Way](http://www.eonclash.com/Tutorials/Multithreading/MartinHarvey1.1/ToC.html) from Martin Harvey. – NGLN Jan 26 '12 at 21:42
  • 2
    'Here are some possible ways I can think of to 'track' these threads'. May I add another - don't do it at all. If you don't do it, it can't go wrong. Micro-managing threads is a really, really bad idea. Create them, passing the task P-C queue as a parameter, then forget them. – Martin James Jan 26 '12 at 21:43
  • 1
    @DavidHeffernan - you are recommending a sleep loop? – Martin James Jan 26 '12 at 21:51
  • @Martin No. Never. I was referring to Jerry's sleep loop from previous question. – David Heffernan Jan 26 '12 at 21:53
  • @DavidHeffernan - OK. For a moment, I thought that all the beers had at last caught up with me.. – Martin James Jan 26 '12 at 22:01
  • Actually already just downloading the OmniThreadLibrary. and @MartinJames I have to somehow track them - because of my requirement to re-use them. Creating and forgetting about them, as I mentioned, is what I'm used to. But to save on create/free/create/free/create/free on these threads, I should only create them once, or as needed. – Jerry Dodge Jan 26 '12 at 22:05
  • 1
    @Jerry with OTL you won't use threads. The library will manage that and present a higher level abstraction. – David Heffernan Jan 26 '12 at 22:31
  • I've got OTL working fine and am already working on version 3. – Jerry Dodge Jan 27 '12 at 00:30
  • Now I'll accept an answer which uses OTL, and info on how to use OTL to match my scenario. For example, a sample of how to launch a threaded process, keep it alive, periodically feed it requests, and destroy it when decreasing active thread count. – Jerry Dodge Jan 27 '12 at 17:03
  • As stated in the comment, use the OmniThreadLibrary Thread Pool class, and do not attempt to roll your own. – Warren P Jan 27 '12 at 01:28
  • +1 That's a really bold statement :P and don't know which comment you're referring to, but I've already begun version 3 of my project using the OTL. – Jerry Dodge Jan 27 '12 at 01:34
  • If you actually find `TThread` more comfortable to use than OTL you simply haven't spent enough time and effort to learn the latter. – mghie Feb 04 '12 at 15:10
  • @mghie Do I have to learn OTL? I think not. It's more than just a matter of comfort or code - one core reason is because I'm using these treads from a component in my package - I do not want to make my (future distributable) package require OTL. – Jerry Dodge Feb 04 '12 at 17:31

3 Answers3

5

As explained by @NGLN etc, you need to pool some threads and accept that the easiest way to manage thread numbers is to divorce the actual number of threads from the desired number. Adding threads to the pool is easy - just create some more instances, (passing the producer-consumer task input queue as a parameter so that the thread knows what to wait on). If the desired number of threads is less than that currently existing, you could queue up enough 'poison-pills' to kill off the extra threads.

Don't keep any list of thread pointers - it's a load of micro-management hassle that's just not necessary, (and will probably go wrong). All you need to keep is a count of the number of desired threads in the pool so you know what action to take when something changes the 'poolDepth' property.

The event triggers are best loaded into the jobs that are issued to the pool - descend them all from some 'TpooledTask' class that takes an event as a constructor parameter and stores it in some 'FonComplete' TNotifyEvent. The thread that runs the task can call the FonComplete when it's done the job, (with the TpooledTask as the sender parameter) - you don't need to know what thread ran the task.

Example:

    unit ThreadPool;

    interface

    uses
      Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
      Dialogs, StdCtrls, contnrs, syncobjs;


    type

    TpooledTask=class(TObject)
    private
      FonComplete:TNotifyEvent;
    protected
      Fparam:TObject;
      procedure execute; virtual; abstract;
    public
      constructor create(onComplete:TNotifyEvent;param:TObject);
    end;

    TThreadPool=class(TObjectQueue)
    private
      access:TcriticalSection;
      taskCounter:THandle;
      threadCount:integer;
    public
      constructor create(initThreads:integer);
      procedure addTask(aTask:TpooledTask);
    end;

    TpoolThread=class(Tthread)
    private
      FmyPool:TThreadPool;
    protected
      procedure Execute; override;
    public
      constructor create(pool:TThreadPool);
    end;

    implementation

    { TpooledTask }

    constructor TpooledTask.create(onComplete: TNotifyEvent; param: TObject);
    begin
      FonComplete:=onComplete;
      Fparam:=param;
    end;

    { TThreadPool }

    procedure TThreadPool.addTask(aTask: TpooledTask);
    begin
      access.acquire;
      try
        push(aTask);
      finally
        access.release;
      end;
      releaseSemaphore(taskCounter,1,nil); // release one unit to semaphore
    end;

    constructor TThreadPool.create(initThreads: integer);
    begin
      inherited create;
      access:=TcriticalSection.create;
      taskCounter:=createSemaphore(nil,0,maxInt,'');
      while(threadCount<initThreads) do
      begin
        TpoolThread.create(self);
        inc(threadCount);
      end;
    end;

    { TpoolThread }

    constructor TpoolThread.create(pool: TThreadPool);
    begin
      inherited create(true);
      FmyPool:=pool;
      FreeOnTerminate:=true;
      resume;
    end;

procedure TpoolThread.execute;
var thisTask:TpooledTask;
begin
  while (WAIT_OBJECT_0=waitForSingleObject(FmyPool.taskCounter,INFINITE)) do
  begin
    FmyPool.access.acquire;
    try
      thisTask:=TpooledTask(FmyPool.pop);
    finally
      FmyPool.access.release;
    end;
    thisTask.execute;
    if assigned(thisTask.FonComplete) then thisTask.FonComplete(thisTask);
  end;
end;

end.
Martin James
  • 24,453
  • 3
  • 36
  • 60
  • 1
    @Jerry - in answer to your next question 'How can I call TThread.Sychronize if I don't have a thread instance?' - don't do that either. If you need main-thread results, PostMessage the task in the OnComplete handler. – Martin James Jan 26 '12 at 21:59
  • The big part of this is that I have to re-use these threads. Each thread will be created and running non-stop. If it has no requests to process, it will just be dead. But when it's given a task to perform, it will keep being re-used for every task given to it. As mentioned above, your method of 'forgetting' about them is what I'm already used to - but the concept of re-using them is where I'm getting lost. – Jerry Dodge Jan 26 '12 at 22:15
  • Jerry, I usually have a thread safe list (or a queue if you like). Each thread has a link to this queue and sleeps when its empty. As long as there are items to remove from the queue, the threads are busy working. – LU RD Jan 26 '12 at 22:48
  • ..somebody will moan about the Yoda conditional at the top of the thread.. :( – Martin James Jan 26 '12 at 23:07
  • Ok, look here :[tthreadedqueue-not-capable-of-multiple-consumers](http://stackoverflow.com/questions/4856306/tthreadedqueue-not-capable-of-multiple-consumers). This is a sceleton single producer multiple consumer scheme which I use a lot for background tasks. In this particular question I use the `TThreadedQueue` as the thread safe queue, but this still has flaws. So I use a very simple queue protected by a critical section for pushing/popping. – LU RD Jan 26 '12 at 23:08
  • @LURD Borland/CodeGear/Embarcadero have a long history of brilliant compilers and poor/buggy/rewritten/rewrittenAgain thread support. I wrote the TObjectQueue semaphore/CS queue at D3 and haven't bothered to change it since. It seems that they liked the monitors/condvars from pthreads, (despite the fact that they don't work correctly - suffer from spurious wakeups and need while loops), and decided to have a go at it. Sure enough... – Martin James Jan 26 '12 at 23:16
  • Example alert! Note that, in the thread, the 'access' CS is kept locked during the execution of the task. This is unnecessary and very bad for contention - the CS should be released as soon as the task is popped off the queue and before calling its 'execute' method. That's what happens when you cobble together an example in 15 mins:( I edited in this change. – Martin James Jan 26 '12 at 23:56
  • +1 Nice example. I never knew of such a `TObjectQueue` to work with. That changes everything, thanks! – Jerry Dodge Jan 27 '12 at 00:33
  • TObjectQueue is very useful indeed, especially when CS-protected and with a counting semaphore, (ie. producer-consumer queue). I use it a lot for inter-thread comms and object pools, (no more continual create/destroy of large buffer objects). – Martin James Jan 27 '12 at 00:51
4

You can implement FreeNotify Message in your request queue and when Worker Thread receieve this message free themselves. In your example when you decrease number of threads from 5 to 3 just put 2 FreeNotify messages in your queue and 2 worker threads will be free.

  • 1
    Yes - easiest way. No terminate/WaitFor/OnTerminate, (ie. no deadlock:). – Martin James Jan 26 '12 at 23:21
  • +1 Sounds very likely a fair idea. All I would have to figure out is how to decide *which* 2 of the 5 are most applicable to destroy. In other words, if I have 5 threads, 3 of them are busy and 2 are idle, then I'm going to want to destroy those 2 that are idle. Don't need help with that, just to point it out... – Jerry Dodge Jan 26 '12 at 23:57
  • 1
    You don't have to figure out anything. If you have 5 threads, 3 are busy and you want to kill 3, push on 3 poison pills. The two threads that are idle will get their suicide request immediately and snuff it. The other pill remains on the queue until another thread finished its task, gets the last dose and dies. That leaves 2 threads - job done! – Martin James Jan 27 '12 at 00:04
  • 1
    +1 ..if there's any great 'secret guru/black art' about multiThreading, it's all about designing to not do stuff. If you do stuff, it will have bugs and go wrong – Martin James Jan 27 '12 at 00:08
  • I think why I'm confused is because I only have 1 type of 'message' I send to these threads, I don't send different types of 'messages' other than to perform 1 single task. Whereas your examples seem to assume that I send different messages (like `ProcThis`, `ProcThat`, `ProcThis`, `ProcAnother`, `ProcKill`) – Jerry Dodge Jan 27 '12 at 01:04
  • 1
    @JerryDodge - OK, only one message - it wasn't clear from your OP. You can still easily kill off pooled threads by queueing a 'nil' and, in your thread/s, 'if not assigned(message) then exit;' – Martin James Jan 27 '12 at 01:21
3

About your problem with the decrement of active threads: sorry, but you simply have to decide for yourself. Either free the unwanted threads immediately (which terminates them at the earliest possible moment), or let them run until they are finished (which terminates them after all work is done). It is your choice. Of course you have to separate the variable for the wished number from that of the actual number of threads. The problem for updating the actual number of threads variable (could simply be a List.Count) are for both exactly the same since either solution will require some time.

And on management of multiple threads: you can study this answer which stores the threads in a TList. It needs a little tweaking for you specific wish list though, please shout in case of need of assistance with that. Also, there are of course more possible implementations which can be derived from the use of the default TThread. And note that there exist other (multi)thread libraries, but I have never had the need to use them.

Community
  • 1
  • 1
NGLN
  • 43,011
  • 8
  • 105
  • 200
  • 1
    There's no difference between freeing immediately and waiting until they are finished. `Destroy` calls `WaitFor`. – David Heffernan Jan 26 '12 at 21:28
  • 2
    FreeOnTerminate:=true. Pass a poison pill task on the task queue. No flag, no TThread.WaitFor, no need to access the TThread instance, so no list of threads needed, so no thread-list management needed, so most of the dodgy deadlock-generating code eliminated. – Martin James Jan 26 '12 at 21:49
  • @David That depends on the task of the Thread. If the thread has a loop which nicely checks for the Terminated flag, then there is. If the thread does only one thing and setting Terminated to False has no influence on running time, then you are right, but then I also do not understand OP's question. – NGLN Jan 26 '12 at 21:51
  • @Martin FreeOnTerminate indeed is very handy, but be careful to set it false when you free a thread manually (that is: if you have a reference at all). – NGLN Jan 27 '12 at 08:55
  • 1
    I've switched back to the `TThread` because the OTL is very uncomfortable to use :) – Jerry Dodge Feb 04 '12 at 10:16
  • @Jerry: not to get all chatting here, but can you briefly describe what you find uncomfortable? I don't fully use all features of the OTL, but I find it quite pleasant to use. – Wouter van Nifterick Feb 04 '12 at 10:25
  • @WoutervanNifterick It's being able to encapsulate the entire thread in one object and not depend on this and that and then some. It's already there and compiled in my project, why add more gibberish to increase my exe size? That's why. – Jerry Dodge Feb 04 '12 at 11:33