Recent

Author Topic: simple thread handling for beginners in Lazarus trunk. Plz review  (Read 771 times)

Thaddy

  • Hero Member
  • *****
  • Posts: 16158
  • Censorship about opinions does not belong here.
While discussing with a real beginner yesterday evening and this night I came up with a simple example to explain the basics of threading in an easy way. It makes use of some features that were not available in Martin Harvey' s time, so it may be a good addition for introducing threads.
This example requires trunk/main, though, and I left out timeouts on purpose.
I would like to know if the example is readable and contains no erors in the comments:
Code: Pascal  [Select][+][-]
  1. program testsimplethread;
  2. {
  3.   This a demo how to use the simple thread handling in FreePascal 3.3.1
  4.   or higher.
  5.   It consists of four threads:
  6.   - The main thread
  7.   - Two worker threads
  8.   - And a controller thread
  9.  
  10.   This pattern is such that the main thread needs to keep track
  11.   of just one thread: the controller thread.
  12.   The controller thread performs the waitfors for all
  13.   worker threads. It only returns if all worker threads
  14.   are finished.
  15.   At program end, the main program just needs to check the waitfor
  16.   for the controller thread.
  17.   Since here all threads are started from the main thread,
  18.   checking and possibly waiting for the controller thread
  19.   guarantees that all threads have finished.
  20.   This resembles Windows WaitForMultipleObjects,
  21.   but in a cross-platform way.
  22.  
  23.   Warning:
  24.   On Windows and Linux 64 bit platforms, the code below is always
  25.   thread safe, even without the synchronizer, but that may be not the
  26.   case for other platforms.
  27.   If you need to exchange other data between threads, you need to
  28.   synchronize it, unless you know it is also thread safe.
  29.   Here consoleIO, gettickcount64, sleep and reading start are all
  30.   thread safe for Windows and Linux 64 bit.
  31.   On 32 bit platforms this is not the case, so I use a
  32.   TMultiReadExclusiveWriteSynchronizer anyway to show best practise.
  33.   In this particular code that does not matter, though:
  34.   You could remove the synchonizer.
  35. }
  36. {$mode objfpc}
  37. {$modeswitch anonymousfunctions}
  38. uses {$ifdef unix}cthreads,{$endif}sysutils,classes;
  39. var
  40.   Sync:IReadWriteSync; { this is a COM interface }
  41.   WorkerThread1,
  42.   WorkerThread2,
  43.   ControllerThread:TThread;
  44.   start:int64;
  45. begin
  46.   { Create a synchronizer, despite its long name very lightweight }
  47.   Sync:=TMultiReadExclusiveWriteSynchronizer.create;
  48.   { measure starttime in ticks, the sync is not really necessary in this
  49.     case, because it is the only write and the threads are not started }
  50.   Sync.BeginWrite;
  51.   start := gettickcount64;
  52.   Sync.EndWrite;
  53.   writeln('Program start time saved');
  54.   { thread with some work load }
  55.   WorkerThread1:=TThread.executeinthread(procedure
  56.                              begin
  57.                                writeln('Test the workerthread1');
  58.                                sleep(2000);
  59.                                Sync.BeginRead;
  60.                                writeln('WorkerThread1 done',  ' took:', gettickcount64 - start);
  61.                                Sync.EndRead;
  62.                              end);
  63.   { thread with not much work load }
  64.   WorkerThread2:=TThread.executeinthread(procedure
  65.                              begin
  66.                                writeln('Test the workerthread2');
  67.                                Sync.BeginRead;
  68.                                writeln('WorkerThread2 done', ' took:', gettickcount64 - start);
  69.                                Sync.EndRead;
  70.                              end);
  71.   { a controller thead that does the waitfor for all worker threads  
  72.     if you do that in the main thread waitfor is blocking it, so
  73.     this is an easy way out. }                        
  74.   ControllerThread:=TThread.executeinthread(procedure
  75.                              begin
  76.                                writeln('Test the ControllerThread');
  77.                                WorkerThread2.waitfor;                          
  78.                                WorkerThread1.waitfor;
  79.                                Sync.BeginRead;
  80.                                writeln('ControllerThread done', ' took:', gettickcount64 - start);
  81.                                Sync.EndRead;
  82.                              end);
  83.   { simulate main workload }
  84.   writeln('perform main workload');                          
  85.   sleep(10000);
  86.   { wait for the controller thread to finish
  87.     but only if it is still running }
  88.   writeln('Main workload finished', ' took:', gettickcount64 - start);
  89.   if not ControllerThread.finished then ControllerThread.waitfor;
  90.   writeln('Main thread finished');    
  91.   writeln;
  92.   writeln('The work load is more or less time defined so let''s analyze:');                
  93.   writeln;
  94.   writeln('WorkerThread1 should cost about 2000 ticks');                
  95.   writeln('WorkerThread2 should cost close to nothing');                
  96.   writeln('ControllerThread does the waitfor''s, so should take the ticks from WorkerThread1 and WorkerThread2 combined');  
  97.   writeln('plus some ticks caused by context switching or the system');  
  98.   writeln;              
  99.   writeln('The whole program should finish in about 10 seconds (the workload of the main thread)');                
  100.   writeln('but we performed much more work!');                              
  101. end.

for reference of course https://www.seti.net/engineering/threads/threads.php#:~:text=Introduction.%20This%20guide%20is%20intended%20for%20anyone%20who
« Last Edit: October 08, 2024, 09:41:55 am by Thaddy »
If I smell bad code it usually is bad code and that includes my own code.

cdbc

  • Hero Member
  • *****
  • Posts: 1646
    • http://www.cdbc.dk
Re: simple thread handling for beginners in Lazarus trunk. Plz review
« Reply #1 on: October 08, 2024, 10:01:14 am »
Hi Thaddy
Right, nifty little program, me likey  :)
Your comments are 'spot on' and your code does exactly, what's advertised =^
My only small reservation, would be the use of /trunk/, when I read the forum, it strikes me, that most of the beginners are using what their distro offers them, or on winders, most download the stable releases...
In my /trunk/ from yesterday, it compiles and runs just fine  8)
I especially like your use of the MREW  ;)
Regards Benny
If it ain't broke, don't fix it ;)
PCLinuxOS(rolling release) 64bit -> KDE5 -> FPC 3.2.2 -> Lazarus 2.2.6 up until Jan 2024 from then on it's: KDE5/QT5 -> FPC 3.3.1 -> Lazarus 3.0

Thaddy

  • Hero Member
  • *****
  • Posts: 16158
  • Censorship about opinions does not belong here.
Re: simple thread handling for beginners in Lazarus trunk. Plz review
« Reply #2 on: October 08, 2024, 10:15:47 am »
The beginner comes from Delphi and asked about anonymous methods, so I gave her trunk/main.
She's not a total programming noob, but she is interested in multiplatform rad programming.
Picks it up pretty fast.
If I smell bad code it usually is bad code and that includes my own code.

Thaddy

  • Hero Member
  • *****
  • Posts: 16158
  • Censorship about opinions does not belong here.
Re: simple thread handling for beginners in Lazarus trunk. Plz review
« Reply #3 on: October 08, 2024, 11:25:42 am »
Benny, what were the values? exact or close as I advertised? On what hardware?
Just curious.
If I smell bad code it usually is bad code and that includes my own code.

cdbc

  • Hero Member
  • *****
  • Posts: 1646
    • http://www.cdbc.dk
Re: simple thread handling for beginners in Lazarus trunk. Plz review
« Reply #4 on: October 08, 2024, 11:43:55 am »
Hi
Here's the output:
Code: Bash  [Select][+][-]
  1. [bc@red threading_for_noobs]$ ./testsimplethread
  2. Program start time saved
  3. Test the workerthread1
  4. Test the workerthread2
  5. perform main workload
  6. WorkerThread2 done took:1
  7. Test the ControllerThread
  8. WorkerThread1 done took:2001
  9. ControllerThread done took:2001
  10. Main workload finished took:10001
  11. Main thread finished
  12.  
  13. The work load is more or less time defined so let's analyze:
  14.  
  15. WorkerThread1 should cost about 2000 ticks
  16. WorkerThread2 should cost close to nothing
  17. ControllerThread does the waitfor's, so should take the ticks from WorkerThread1 and WorkerThread2 combined
  18. plus some ticks caused by context switching or the system
  19.  
  20. The whole program should finish in about 10 seconds (the workload of the main thread)
  21. but we performed much more work!
  22. [bc@red threading_for_noobs]$
  23.  
The lappy is a HP Notebook 15(something) ...it's shiny red  :D
4 core AMD, 8gb ram, running PCLinuxOS 07.10.2024, KDE->QT5
fpc= 3.3.1[07-10-2024], laz=4.99[07-10-2024]
HTH
Regards Benny
If it ain't broke, don't fix it ;)
PCLinuxOS(rolling release) 64bit -> KDE5 -> FPC 3.2.2 -> Lazarus 2.2.6 up until Jan 2024 from then on it's: KDE5/QT5 -> FPC 3.3.1 -> Lazarus 3.0

carl_caulkett

  • Hero Member
  • *****
  • Posts: 649
Re: simple thread handling for beginners in Lazarus trunk. Plz review
« Reply #5 on: October 08, 2024, 11:53:07 am »
* Mac Mini M1
* macOS 14.6.1
* Lazarus 3.99
* FPC 3.3.1

On my Mac Mini M1, with 16GB of RAM, this is what is outputted...
Code: Bash  [Select][+][-]
  1. [Tue Oct 08 2024 10:47am (BST+0100)]
  2. ~/Code/fpc/testsimplethread  ./testsimplethread
  3. Program start time saved
  4. perform main workload
  5. Test the workerthread1
  6. Test the ControllerThread
  7. Test the workerthread2
  8. WorkerThread2 done took:0
  9. WorkerThread1 done took:2005
  10. Main workload finished took:10005
  11.  

The app seems to hang there...
« Last Edit: October 08, 2024, 12:02:08 pm by carl_caulkett »
"It builds... ship it!"

carl_caulkett

  • Hero Member
  • *****
  • Posts: 649
Re: simple thread handling for beginners in Lazarus trunk. Plz review
« Reply #6 on: October 08, 2024, 12:09:26 pm »
I tweaked the code, to try to investigate what was going on, to...
Code: Pascal  [Select][+][-]
  1.   writeln('Main workload finished', ' took:', gettickcount64 - start);
  2.   if not ControllerThread.finished then
  3.   begin
  4.     writeln('waiting for ControllerThread.finished');
  5.     ControllerThread.waitfor;
  6.   end;
  7.   writeln('Main thread finished');
  8.  

...but it seemed to work, this time! I don't think my tweak could have caused this, but threads are not my area of expertise  ;)
Code: Bash  [Select][+][-]
  1. [130 - ][Tue Oct 08 2024 11:06am (BST+0100)]    took 18m49s
  2. ~/Code/fpc/testsimplethread  ./testsimplethread
  3. Program start time saved
  4. Test the workerthread1
  5. perform main workload
  6. Test the ControllerThread
  7. Test the workerthread2
  8. WorkerThread2 done took:0
  9. WorkerThread1 done took:2005
  10. Main workload finished took:10005
  11. Main thread finished
  12.  
  13. The work load is more or less time defined so let's analyze:
  14.  
  15. WorkerThread1 should cost about 2000 ticks
  16. WorkerThread2 should cost close to nothing
  17. ControllerThread does the waitfor's, so should take the ticks from WorkerThread1 and WorkerThread2 combined
  18. plus some ticks caused by context switching or the system
  19.  
  20. The whole program should finish in about 10 seconds (the workload of the main thread)
  21. but we performed much more work!
  22.  
  23. [Tue Oct 08 2024 11:06am (BST+0100)]    took 10s
  24. ~/Code/fpc/testsimplethread
  25.  
« Last Edit: October 08, 2024, 12:12:03 pm by carl_caulkett »
"It builds... ship it!"

Thaddy

  • Hero Member
  • *****
  • Posts: 16158
  • Censorship about opinions does not belong here.
Re: simple thread handling for beginners in Lazarus trunk. Plz review
« Reply #7 on: October 08, 2024, 12:35:51 pm »
They are my area of expertise, earned my pension.. ::) Still even an expert needs reflection on threading since it stays very hard to not make mistakes.
The 'hang' is not a hang,but the main thread performing its 10 second work load simulation.
You should not tweak the code, at that point, it is correct.
The whole idea is a program with four threads that runs always for about 10 seconds (10000) and maybe a few ticks more in total.
You can replace the 10 second sleep for the main program with any code you like and the other sleeps also with your own code. In that case it takes more effort to demonstrate the advantage of using threads.
This is explicitly written for novices in thread programming, hence I used load simulation to show the effect of threads: you can do many jobs in about equal time as a single threaded program, e.g.:
Code: Pascal  [Select][+][-]
  1. {$mode objfpc}
  2. uses sysutils;
  3. begin
  4.   sleep(10000);
  5.   writeln ('woke up');
  6. end.
Would in your opinion also hang for 10 seconds...... O:-) If you would have understood the comments in the source you would have known, but still I will adapt it a bit after your misunderstanding of it.
« Last Edit: October 08, 2024, 12:48:14 pm by Thaddy »
If I smell bad code it usually is bad code and that includes my own code.

cdbc

  • Hero Member
  • *****
  • Posts: 1646
    • http://www.cdbc.dk
Re: simple thread handling for beginners in Lazarus trunk. Plz review
« Reply #8 on: October 08, 2024, 12:41:29 pm »
Hi
Quote
Still even an expert needs reflection on threading since it stays very hard to not make mistakes.
True that!
Regards Benny
If it ain't broke, don't fix it ;)
PCLinuxOS(rolling release) 64bit -> KDE5 -> FPC 3.2.2 -> Lazarus 2.2.6 up until Jan 2024 from then on it's: KDE5/QT5 -> FPC 3.3.1 -> Lazarus 3.0

carl_caulkett

  • Hero Member
  • *****
  • Posts: 649
Re: simple thread handling for beginners in Lazarus trunk. Plz review
« Reply #9 on: October 08, 2024, 01:27:47 pm »
They are my area of expertise, earned my pension.. ::) Still even an expert needs reflection on threading since it stays very hard to not make mistakes.

As long as you mean "reflection", as opposed to "Reflection", which is a whole other story ;)

When I said it hung, I meant that the app seemed to get stuck somewhere for almost a minute, before I terminated with a Ctrl-C.
When I changed the code as indicated, the app ran is full.
I've reverted the code back to your original form, and it's still not finishing...
Code: Bash  [Select][+][-]
  1. [Tue Oct 08 2024 11:06am (BST+0100)]    took 10s
  2. ~/Code/fpc/testsimplethread  ./testsimplethread
  3. Program start time saved
  4. perform main workload
  5. Test the workerthread2
  6. Test the workerthread1
  7. WorkerThread2 done took:0
  8. Test the ControllerThread
  9. WorkerThread1 done took:2006
  10. Main workload finished took:10005
  11. GETS STUCK HERE!
  12.  

Once again, changing the code to...
Code: Pascal  [Select][+][-]
  1.   writeln('Main workload finished', ' took:', gettickcount64 - start);
  2.   if not ControllerThread.finished then
  3.   begin
  4.     writeln('waiting for ControllerThread.finished');
  5.     ControllerThread.waitfor;
  6.   end;
  7.   writeln('Main thread finished');
  8.  

seems to make it work, but I fully appreciate that with threads, the normal, apparent, programming rules of cause and effect do not necessarily apply, a bit like 4 dimensional chess ;)
"It builds... ship it!"

Thaddy

  • Hero Member
  • *****
  • Posts: 16158
  • Censorship about opinions does not belong here.
Re: simple thread handling for beginners in Lazarus trunk. Plz review
« Reply #10 on: October 08, 2024, 02:38:14 pm »
I have prepared an example with real load, where the worker threds perform both 100000 tasks.
Code: Pascal  [Select][+][-]
  1. program simplethread2.pas;
  2. {$mode objfpc}
  3. {$modeswitch anonymousfunctions}
  4. uses {$ifdef unix}cthreads,{$endif}sysutils,classes;
  5. var
  6.   Sync:IReadWriteSync;
  7.   WorkerThread1,
  8.   WorkerThread2,
  9.   ControllerThread:TThread;
  10.   start:int64;
  11. begin
  12.   Sync:=TMultiReadExclusiveWriteSynchronizer.create;
  13.   Sync.BeginWrite;
  14.   start := gettickcount64;
  15.   Sync.EndWrite;
  16.   writeln('Program start time saved');
  17.   WorkerThread1:=TThread.executeinthread(procedure
  18.                              var
  19.                                s:string = 'Hey Main, I am still busy....';
  20.                                i:integer;
  21.                              begin
  22.                                for i := 0 to 100000 do if i mod 100 = 0 then
  23.                                begin
  24.                                  Sync.BeginWrite;
  25.                                  writeln(s);
  26.                                  Sync.EndWrite;
  27.                                end;                              
  28.                                Sync.BeginRead;
  29.                                writeln('WorkerThread1 done',  ' took:', gettickcount64 - start);
  30.                                Sync.EndRead;
  31.                              end);
  32.   WorkerThread2:=TThread.executeinthread(procedure
  33.                              var
  34.                                s:string = 'Hey, One,Still chatting?....';
  35.                                i:integer;
  36.                              begin
  37.                                for i := 0 to 100000 do if i mod 100 = 0 then
  38.                                begin
  39.                                  Sync.BeginWrite;
  40.                                  writeln(s);
  41.                                  Sync.EndWrite;
  42.                                end;                              
  43.                                Sync.BeginRead;
  44.                                writeln('WorkerThread2 done',  ' took:', gettickcount64 - start);
  45.                                Sync.EndRead;
  46.                              end);
  47.   ControllerThread:=TThread.executeinthread(procedure
  48.                              begin
  49.                                writeln('Test the ControllerThread');
  50.                                                            WorkerThread1.waitfor;
  51.                                WorkerThread2.waitfor;                          
  52.                                                            Sync.BeginRead;
  53.                                writeln('ControllerThread done, handing over to main', ' took:', gettickcount64 - start);
  54.                                Sync.EndRead;
  55.                              end);
  56.   writeln('perform main workload');                          
  57.   sleep(10000);
  58.   writeln('Main workload finished', ' took:', gettickcount64 - start);
  59.   if not ControllerThread.finished then ControllerThread.waitfor;
  60.   writeln('Main thread finished in ', gettickcount64 - start);    
  61. end.

See?  ;D still 10 seconds....
If I smell bad code it usually is bad code and that includes my own code.

carl_caulkett

  • Hero Member
  • *****
  • Posts: 649
Re: simple thread handling for beginners in Lazarus trunk. Plz review
« Reply #11 on: October 08, 2024, 04:10:57 pm »
Happy to report that on my Mac, it works perfectly :D

Code: Bash  [Select][+][-]
  1. [Tue Oct 08 2024 12:24pm (BST+0100)]    took 10s
  2. ~/Code/fpc/testsimplethread  ./testsimplethread
  3. Program start time saved
  4. Hey Main, I am still busy....
  5. perform main workload
  6. Hey, One,Still chatting?....
  7. Hey, One,Still chatting?....
  8. Hey, One,Still chatting?....
  9. Test the ControllerThread
  10. Hey, One,Still chatting?....
  11.  
  12. substantial ---8<--- age ;)
  13.  
  14. Hey, One,Still chatting?....
  15. Hey, One,Still chatting?....
  16. WorkerThread2 done took:9
  17. Main workload finished took:10003
  18. Main thread finished in 10003
  19.  
"It builds... ship it!"

Thaddy

  • Hero Member
  • *****
  • Posts: 16158
  • Censorship about opinions does not belong here.
Re: simple thread handling for beginners in Lazarus trunk. Plz review
« Reply #12 on: October 09, 2024, 09:34:46 am »
Carl, comment out the sleep(10000) to see the performance of the threads in the context of a whole program.
If I smell bad code it usually is bad code and that includes my own code.

 

TinyPortal © 2005-2018