Author Topic: Threading  (Read 346 times)

0 Members and 1 Guest are viewing this topic.

Offline justsomeguy

  • Newbie
  • Posts: 19
Threading
« on: May 03, 2021, 03:31:18 PM »
Hello

I'm starting down the rabbit-hole of using threads in my 2d physics engine. I'm aware that multi-threading is not supported, nor probably ever supported by QB64, however, I'm trying to see if it is possible to do by using the pthreads library.

When you are creating threads in pthread, you must provide a void pointer to the sub/function that you are trying to create multiple threads of. I'm at loss on how to get these pointers. It seems that it is a one way street between the libraries and your QB program, no way for them to call back your program.

Is there some clever workaround? I read a post somewhere that someone had some limited success with the windows multi-threading in QB64, but they only got to the demo stage.

Any help in this regard will be appreciated. Thanks!

Offline SpriggsySpriggs

  • QB64 Developer
  • Forum Resident
  • Posts: 935
  • If you're API and you know it clap your hands
    • My GitHub
Re: Threading
« Reply #1 on: May 03, 2021, 03:40:09 PM »
@justsomeguy I believe we can use threads in QB64 but it could be quite a labor to do so if you want to use it purely (or almost purely) in the QB64 IDE. It's probably quite simple when doing it by using a header file. This idea has been nagging my brain for quite some time. I'm planning on looking into this at some point both as a header and as (mostly) QB64 syntax. If you look at the page from the MSDN on threads here, it looks like it might not be too terrible.

Offline justsomeguy

  • Newbie
  • Posts: 19
Re: Threading
« Reply #2 on: May 03, 2021, 04:07:52 PM »
Okay, so using a header file, how would you call a function/sub that you wrote in QB?

Offline SpriggsySpriggs

  • QB64 Developer
  • Forum Resident
  • Posts: 935
  • If you're API and you know it clap your hands
    • My GitHub
Re: Threading
« Reply #3 on: May 03, 2021, 04:56:10 PM »
Here is an example in which a QB64 function is used as a CALLBACK:

Code: C++: [Select]
  1. ptrszint FUNC_WINDOWPROC(ptrszint*_FUNC_WINDOWPROC_OFFSET_HWND,uint32*_FUNC_WINDOWPROC_ULONG_UMSG,uptrszint*_FUNC_WINDOWPROC_UOFFSET_WPARAM,ptrszint*_FUNC_WINDOWPROC_OFFSET_LPARAM);
  2. //^^this is the declaration of the QB64 function WindowProc%&, as it would appear after being translated by the IDE^^
  3.  
  4. LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
  5.  return FUNC_WINDOWPROC((ptrszint *) (& hwnd), & uMsg, & wParam, (ptrszint *) (& lParam));
  6. //^^returning the value from the QB64 function^^
  7. }
  8.  
  9. void * GetWindowProc() {
  10.  return (void *) WindowProc;
  11. //^^getting the pointer to the function that could be used to call the QB64 function^^
  12. }
  13.  
« Last Edit: May 03, 2021, 04:59:18 PM by SpriggsySpriggs »

Offline justsomeguy

  • Newbie
  • Posts: 19
Re: Threading
« Reply #4 on: May 03, 2021, 05:14:23 PM »
Thank you! I will try to make a more generic version of this and try it.

Offline Galleon

  • QB64 Developer
  • Newbie
  • Posts: 17
Re: Threading
« Reply #5 on: May 04, 2021, 07:56:56 AM »
Calling multiple QB64 subs/functions in parallel without a lot of changes to the compiler is not going to work.
Have a look at what ...\internal\temp\main.txt looks like after compiling this simple program...
Code: QB64: [Select]
  1. c = addnums(5, 6)
  2. FUNCTION addnums (a, b)
  3.     addnums = a + b
  4.  
You will see a lot of extra code and the beginning and end of a function called FUNC_ADDNUMS.
That code is working with a range of global variables and if two SUBs/FUNCTIONs were called at the same time it's going to be very bad.
If you really need that extra power, may I suggest running multiple exes at once and using TCP/IP communication between them.

That said, if you do proceed down this route, I imagine you have more chance of inventing general AI than many of the researchers in that field.
« Last Edit: May 04, 2021, 08:01:07 AM by Galleon »

Offline justsomeguy

  • Newbie
  • Posts: 19
Re: Threading
« Reply #6 on: May 04, 2021, 08:25:14 AM »
Quote
Calling multiple QB64 subs/functions in parallel without a lot of changes to the compiler is not going to work.

Hmmm that is disappointing. Would it be equally impossible if I wrote this instead?

Code: QB64: [Select]
  1. c = addnumsThread0(5, 6)
  2. d = addnumsThread1(4, 8)
  3. e = addnumsThread2(1, 3)
  4.  
  5. FUNCTION addnumsThread0 (a, b)
  6.     addnums = a + b
  7.  
  8. FUNCTION addnumsThread1 (a, b)
  9.     addnums = a + b
  10.  
  11. FUNCTION addnumsThread2 (a, b)
  12.     addnums = a + b

I know this looks foolish. But maybe a slightly altered approach?
Quote
If you really need that extra power, may I suggest running multiple exes at once and using TCP/IP communication between them.

That is a good idea. I have not really explored that.

Offline FellippeHeitor

  • QB64 Developer
  • Forum Resident
  • Posts: 2883
  • Let it go, this too shall pass.
    • QB64.org
Re: Threading
« Reply #7 on: May 04, 2021, 08:57:44 AM »
Calling multiple QB64 subs/functions in parallel without a lot of changes to the compiler is not going to work.

You now have me wondering if ON TIMER calling Subs is all good to go.

Code: QB64: [Select]
  1. On Timer(t, 1) doIt
  2.  
  3. goLoop
  4.  
  5. Sub goLoop
  6.     Do
  7.         Print "looping... ";
  8.         _Limit 10
  9.     Loop
  10.  
  11. Sub doIt
  12.     Color 5
  13.     Print "--> Doing it..."
  14.     Color 7
« Last Edit: May 04, 2021, 09:12:18 AM by FellippeHeitor »

Offline justsomeguy

  • Newbie
  • Posts: 19
Re: Threading
« Reply #8 on: May 04, 2021, 09:42:17 AM »
I guess ultimately my thought was along the lines of, if running the exact same sub/function concurrently is problematic, then what about different sub/functions concurrently?

I suspect 'ON TIMER' might be okay since it is not actually concurrent.





Offline justsomeguy

  • Newbie
  • Posts: 19
Re: Threading
« Reply #9 on: May 05, 2021, 10:20:44 AM »
Hello

Well, I got threads to work...somewhat...mostly...kinda. I've run it many, many times and I only get core dumps occasionally ;). Its obviously not as stable as I like, but It is merely a proof of concept. Its okay for testing and it has taught me a lot.

I'm using a linux machine so I'm using the pthread library, but I imagine the process would be similar to make it work in Windows, no idea about Mac. To keep it simple I kept all of the pthread stuff in the header. Functions are of course in the QB program.

If you would like to try it and you are on a Linux box then here is the source.

This is the header required it must be saved as "qbthread2.h" in your folder with the program.
Code: QB64: [Select]
  1. #include "pthread.h"
  2.  
  3. typedef struct thread_data {
  4.    int a;
  5.    float result;
  6. } thread_data;
  7.  
  8.  
  9. float FUNC_THREAD0(int16*_FUNC_THREAD0_INTEGER_A);
  10. float FUNC_THREAD1(int16*_FUNC_THREAD1_INTEGER_A);
  11. float FUNC_THREAD2(int16*_FUNC_THREAD2_INTEGER_A);
  12.  
  13. static pthread_mutex_t mutex0;
  14. static pthread_mutex_t mutex1;
  15. static pthread_mutex_t mutex2;
  16.  
  17. void* RunThread0(void *arg){
  18.         pthread_mutex_lock(&mutex0);
  19.         thread_data *tdata=(thread_data *)arg;
  20.         int a = tdata->a;
  21.         tdata->result = FUNC_THREAD0((int16*)&a);
  22.         pthread_mutex_unlock(&mutex0);
  23.         pthread_exit(NULL);
  24. }
  25.  
  26. void* RunThread1(void *arg){
  27.         pthread_mutex_lock(&mutex1);
  28.         thread_data *tdata=(thread_data *)arg;
  29.         int a = tdata->a;
  30.         tdata->result = FUNC_THREAD1((int16*)&a);
  31.         pthread_mutex_unlock(&mutex1);
  32.         pthread_exit(NULL);
  33. }
  34.  
  35. void* RunThread2(void *arg){
  36.         pthread_mutex_lock(&mutex2);
  37.         thread_data *tdata=(thread_data *)arg;
  38.         int a = tdata->a;
  39.         tdata->result = FUNC_THREAD2((int16*)&a);
  40.         pthread_mutex_unlock(&mutex2);
  41.         pthread_exit(NULL);
  42. }
  43.  
  44. long invokeThreadsMethodOne(int a, int b, int c){
  45.         pthread_t threads01;
  46.         pthread_t threads02;
  47.         pthread_t threads03;
  48.        
  49.         thread_data tdata1;
  50.         thread_data tdata2;
  51.         thread_data tdata3;
  52.        
  53.         tdata1.a = a;
  54.         tdata2.a = b;
  55.         tdata3.a = c;
  56.        
  57.         int t1 = pthread_create(&threads01, NULL, RunThread0, (void *)&tdata1);
  58.         if (t1 != 0)
  59.     {
  60.                 cout << "Error in thread creation: " << t1 << endl;
  61.     }
  62.    
  63.     int t2 = pthread_create(&threads02, NULL, RunThread1, (void *)&tdata2);
  64.         if (t2 != 0)
  65.     {
  66.                 cout << "Error in thread creation: " << t2 << endl;
  67.     }
  68.    
  69.     int t3 = pthread_create(&threads03, NULL, RunThread2, (void *)&tdata3);
  70.         if (t3 != 0)
  71.     {
  72.                 cout << "Error in thread creation: " << t3 << endl;
  73.     }
  74.    
  75.     void* status1;
  76.     void* status2;
  77.     void* status3;
  78.    
  79.         t1 = pthread_join(threads01, &status1);
  80.         if (t1 != 0)
  81.         {
  82.                 cout << "Error in thread join: " << t1 << endl;
  83.         }
  84.        
  85.         t2 = pthread_join(threads02, &status2);
  86.         if (t2 != 0)
  87.         {
  88.                 cout << "Error in thread join: " << t2 << endl;
  89.         }
  90.  
  91.         t3 = pthread_join(threads03, &status3);
  92.         if (t3 != 0)
  93.         {
  94.                 cout << "Error in thread join: " << t3 << endl;
  95.         }
  96.     return tdata1.result + tdata2.result + tdata3.result;
  97. }
  98.  
  99. long invokeThreadsMethodTwo(int a, int b, int c)
  100. {
  101.     pthread_t threads[3];
  102.     thread_data tdata[3];
  103.     tdata[0].a = a;
  104.     tdata[1].a = b;
  105.     tdata[2].a = c;
  106.  
  107.     for (long int i = 0 ; i < 3 ; ++i)
  108.     {
  109.         int t = pthread_create(&threads[i], NULL, RunThread0, (void *)&tdata[i]);
  110.  
  111.         if (t != 0)
  112.         {
  113.             cout << "Error in thread creation: " << t << endl;
  114.         }
  115.     }
  116.  
  117.     for(int i = 0 ; i < 3; ++i)
  118.     {
  119.         void* status;
  120.         int t = pthread_join(threads[i], &status);
  121.         if (t != 0)
  122.         {
  123.             cout << "Error in thread join: " << t << endl;
  124.             }
  125.     }
  126.  
  127.     return tdata[0].result + tdata[1].result + tdata[2].result;
  128. }
  129.  

Here is the QBasic portion. It can be saved as whatever you like.

Code: QB64: [Select]
  1. ' a,b,c are integers passed to the individual threads
  2. ' right now only a long value is returned
  3. DECLARE LIBRARY "./qbthread2"
  4.     FUNCTION invokeThreadsMethodOne (BYVAL a AS INTEGER, BYVAL b AS INTEGER, BYVAL c AS INTEGER)
  5.     FUNCTION invokeThreadsMethodTwo (BYVAL a AS INTEGER, BYVAL b AS INTEGER, BYVAL c AS INTEGER)
  6.  
  7. CONST cITERATIONS = 100
  8. CONST cSUBITERATIONS = 1000
  9. CONST cFUNCTIONITERATIONS = 20000
  10.  
  11. i = 0
  12. tmr = TIMER(.001)
  13.     LOCATE 1, 1
  14.     PRINT "Invoke Three Different Functions  --- Iteration:"; i
  15.     z = invokeThreadsMethodOne(cSUBITERATIONS, cSUBITERATIONS, cSUBITERATIONS)
  16.     PRINT "Threads returned:"; z
  17.     i = i + 1
  18. LOOP UNTIL i >= cITERATIONS OR _KEYHIT = 27
  19. PRINT TIMER(.001) - tmr; " Seconds have elapsed."
  20. PRINT "Press Space to continue..."
  21.  
  22. i = 0
  23. tmr = TIMER(.001)
  24.     LOCATE 1, 1
  25.     PRINT "Invoke Same Function Three Times --- Iteration:"; i
  26.     z = invokeThreadsMethodTwo(cSUBITERATIONS, cSUBITERATIONS, cSUBITERATIONS)
  27.     PRINT "Threads returned:"; z
  28.     i = i + 1
  29. LOOP UNTIL i >= cITERATIONS OR _KEYHIT = 27
  30. PRINT TIMER(.001) - tmr; " Seconds have elapsed."
  31. PRINT "Press Space to continue..."
  32.  
  33. i = 0
  34. tmr = TIMER(.001)
  35.     LOCATE 1, 1
  36.     PRINT "Just Call Functions Normally --- Iteration:"; i
  37.     z = thread0(cSUBITERATIONS) + thread1(cSUBITERATIONS) + thread2(cSUBITERATIONS)
  38.     PRINT "Functions returned:"; z
  39.     i = i + 1
  40. LOOP UNTIL i >= cITERATIONS OR _KEYHIT = 27
  41. PRINT TIMER(.001) - tmr; " Seconds have elapsed."
  42.  
  43. '***********************************************************************************
  44. ' Threaded Functions
  45. '***********************************************************************************
  46.  
  47. FUNCTION thread0 (a AS INTEGER)
  48.     DIM AS LONG i, o
  49.     FOR i = 1 TO a * cFUNCTIONITERATIONS
  50.         o = o + 7
  51.     NEXT
  52.     thread0 = o
  53.  
  54. FUNCTION thread1 (a AS INTEGER)
  55.     DIM AS LONG i, o
  56.     FOR i = 1 TO a * cFUNCTIONITERATIONS
  57.         o = o + 7
  58.     NEXT
  59.     thread1 = o
  60.  
  61. FUNCTION thread2 (a AS INTEGER)
  62.     DIM AS LONG i, o
  63.     FOR i = 1 TO a * cFUNCTIONITERATIONS
  64.         o = o + 7
  65.     NEXT
  66.     thread2 = o
  67.  

In the program I tested a couple of different approaches to creating threads to see how it effects stability and speed and compared it to a baseline of just running the functions sequentially.

On my machine which is few years old and not very fast to begin with I got these results.

  • Pthreads opens 3 threads of different functions that perform the same task - 41.03 seconds
  • Pthreads opens 3 threads of a single function - 29.33 seconds
  • Run the functions normally in sequence - 27.54 seconds

Needless to say the result were surprising. Unless there is some flaw in the way I implemented the threads or the test, it appears that running a single sequential thread outperforms three threads. I suspect that the overhead in creating the threads may play a part in it, or more likely my test is garbage. The attached pictures show my CPU utilzation during the tests.

The first method is unorthodox and occassionally returns the wrong result. Clearly a bad idea, but now I know for sure.

I might experiment with more threads to see if results change.




Offline Richard Frost

  • Seasoned Forum Regular
  • Posts: 285
  • Needle nardle noo. - Peter Sellers
Re: Threading
« Reply #10 on: May 05, 2021, 04:22:28 PM »
Impressive attempt.  Threads would be useful for my chess program.

I don't understand what the .001 parameter in your use of TIMER is for.
It works better if you plug it in.

Offline SpriggsySpriggs

  • QB64 Developer
  • Forum Resident
  • Posts: 935
  • If you're API and you know it clap your hands
    • My GitHub
Re: Threading
« Reply #11 on: May 05, 2021, 04:24:42 PM »
@justsomeguy I didn't realize you were using Linux. In that case, look up the pages on fork for another neat thing. I am genuinely impressed by your header file. I'm not at my Linux right now but when I get back from my business trip then I'll for sure give your file a try.
« Last Edit: May 05, 2021, 04:26:45 PM by SpriggsySpriggs »

Offline justsomeguy

  • Newbie
  • Posts: 19
Re: Threading
« Reply #12 on: May 05, 2021, 05:47:08 PM »
Quote
Impressive attempt.  Threads would be useful for my chess program.
Well if my data is correct, which it very well may not be, threading may not help you. It was in fact slower, even when I bumped it up to four threads. Not to mention threading in QB64 is a complete minefield full of "seg faults" and "core dumps." It is not for the faint of heart.

The test I wrote relies heavily with not interacting with QB built-in functions.  Even PRINT will core dump. The idea was to pass a threaded function/sub some arguments, and have it chew on it, and then pass results to some memory that is allocated just for that thread. These functions would have to be very simple and not call anything from the outside.

Quote
I don't understand what the .001 parameter in your use of TIMER is for.

This is from the help for TIMER
Quote
Example 3: Using a DOUBLE variable for TIMER(.001) millisecond accuracy in QB64 throughout the day.

 ts! = TIMER(.001)     'single variable
 td# = TIMER(.001)     'double variable

 PRINT "Single ="; ts!
 PRINT "Double ="; td# 


 Single = 77073.09
 Double = 77073.094

    Explanation: SINGLE variables will cut off the millisecond accuracy returned so DOUBLE variables should be used. TIMER values
    will also exceed INTEGER limits. When displaying TIMER values, use LONG for seconds and DOUBLE for milliseconds.

Granted it was ultimately unneeded.

@SpriggsySpriggs First, Thank you! Without your guidance I would not have been able to even get started. I will look into fork and thanks for complimenting the header.

I have stripped it down to essentials now that I have finished testing some dumb ideas.

This header must be named "qbthread3.h" and be located in the same folder as the source.
Code: QB64: [Select]
  1. #include "pthread.h"
  2.  
  3. typedef struct thread_data {
  4.    int a;
  5.    float result;
  6. } thread_data;
  7.  
  8. #define NUM_OF_THREADS 3
  9.  
  10. float FUNC_THREAD0(int16*_FUNC_THREAD0_INTEGER_A);
  11.  
  12. static pthread_mutex_t mutex0;
  13.  
  14. void* RunThread0(void *arg){
  15.         pthread_mutex_lock(&mutex0);
  16.         thread_data *tdata=(thread_data *)arg;
  17.         int a = tdata->a;
  18.         tdata->result = FUNC_THREAD0((int16*)&a);
  19.         pthread_mutex_unlock(&mutex0);
  20.         pthread_exit(NULL);
  21. }
  22.  
  23.  
  24. long invokeThreads(int a, int b, int c) // you will have to add more arguments for more threads
  25. {
  26.     pthread_t threads[NUM_OF_THREADS];
  27.     thread_data tdata[NUM_OF_THREADS];
  28.     tdata[0].a = a;
  29.     tdata[1].a = b;
  30.     tdata[2].a = c;
  31.     float res = 0;
  32.  
  33.     for (long int i = 0 ; i < NUM_OF_THREADS ; ++i)
  34.     {
  35.         int t = pthread_create(&threads[i], NULL, RunThread0, (void *)&tdata[i]);
  36.  
  37.         if (t != 0)
  38.         {
  39.             cout << "Error in thread creation: " << t << endl;
  40.         }
  41.     }
  42.  
  43.     for(int i = 0 ; i < NUM_OF_THREADS; ++i)
  44.     {
  45.         void* status;
  46.         int t = pthread_join(threads[i], &status);
  47.         if (t != 0)
  48.         {
  49.             cout << "Error in thread join: " << t << endl;
  50.         }
  51.         res += tdata[i].result;
  52.     }
  53.     return res;
  54. }
  55.  

This is the stripped down source. Name it whatever you want.
Code: QB64: [Select]
  1. ' a,b,c are integers passed to the individual threads
  2. ' right now only a long value is returned
  3. DECLARE LIBRARY "./qbthread3"
  4.     FUNCTION invokeThreads (BYVAL a AS INTEGER, BYVAL b AS INTEGER, BYVAL c AS INTEGER)
  5.  
  6. CONST cITERATIONS = 10
  7. CONST cSUBITERATIONS = 10000
  8. CONST cFUNCTIONITERATIONS = 20000
  9.  
  10. i = 0
  11. tmr = TIMER(.001)
  12.     LOCATE 1, 1
  13.     PRINT "Invoke 3 Threads  --- Iteration:"; i
  14.     z = invokeThreads(cSUBITERATIONS, cSUBITERATIONS, cSUBITERATIONS)
  15.     PRINT "Threads returned:"; z
  16.     i = i + 1
  17. LOOP UNTIL i >= cITERATIONS OR _KEYHIT = 27
  18. PRINT TIMER(.001) - tmr; " Seconds have elapsed."
  19. PRINT "Press Space to continue..."
  20.  
  21. i = 0
  22. tmr = TIMER(.001)
  23.     LOCATE 1, 1
  24.     PRINT "Just Call Functions Normally --- Iteration:"; i
  25.     z = thread0(cSUBITERATIONS) + thread0(cSUBITERATIONS) + thread0(cSUBITERATIONS)
  26.     PRINT "Functions returned:"; z
  27.     i = i + 1
  28. LOOP UNTIL i >= cITERATIONS OR _KEYHIT = 27
  29. PRINT TIMER(.001) - tmr; " Seconds have elapsed."
  30.  
  31.  
  32.  
  33. '***********************************************************************************
  34. ' Threaded Functions
  35. '***********************************************************************************
  36.  
  37. FUNCTION thread0 (a AS INTEGER)
  38.     DIM AS LONG i, o
  39.     FOR i = 1 TO a * cFUNCTIONITERATIONS
  40.         o = o + 3
  41.     NEXT
  42.     thread0 = o
  43.  
  44.  


Offline SpriggsySpriggs

  • QB64 Developer
  • Forum Resident
  • Posts: 935
  • If you're API and you know it clap your hands
    • My GitHub
Re: Threading
« Reply #13 on: May 06, 2021, 08:43:01 AM »
I am quite excited to see this code progress. I can't wait to get home from my trip and try your code.

Offline bplus

  • Forum Resident
  • Posts: 6622
  • b = b + ...
Re: Threading
« Reply #14 on: May 06, 2021, 11:45:03 AM »
Impressive attempt.  Threads would be useful for my chess program.

I don't understand what the .001 parameter in your use of TIMER is for.

The .001 parameter tells the function the level of accuracy desired.

Maybe different threads but only one CPU, wouldn't the management of different threads take away from time a huge number crunching algo that Max\min to some high level look ahead needs?