Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Threading in QB64pe (again)
#1
Hello all,

I've been revisiting my earlier attempt at threading in QB64pe. You can look at the old thread at https://qb64forum.alephc.xyz/index.php?topic=3865.0 Thanks to guys that responded to that thread and helped me. 

I'm happy to say I made a bit of progress. I managed to get two separate free running threads to run concurrent with my main QB64 program. They don't do much, but they show its possible.

In order to get it working I had to cheat and use a c header file to make a wrapper for pthreads. This makes the declarations easier.

Code: (Select All)
// pthreadGFXTest.h
// Threading Header
#include "pthread.h"
// Only needed for the SIGTERM Constant
#include <signal.h>
// Initialize Threads
pthread_t thread0;
pthread_t thread1;
// Easy way to determine if a thread is running
bool threadRunning0 = false;
bool threadRunning1 = false;
// Setup Mutexes for each of the threads.
static pthread_mutex_t mutex0;
static pthread_mutex_t mutex1;
// QB's names for the threaded Subs
// You can locate these in your ''qb64pe/internal/temp'' folder.
// I found these in the 'main.txt'
void SUB_LINES();
void SUB_CIRCLES();
// wrap the subs so that you can easily get the void* for pthread
void* RunLines(void *arg){
    SUB_LINES();
}
void* RunCircles(void *arg){
    SUB_CIRCLES();
}
// These are the commands that are accessed by you program
void invokeLines(){
    if (!threadRunning0) {
        int iret = pthread_create( &thread0, NULL, RunLines, NULL);
        pthread_mutex_init(&mutex0, NULL);
        threadRunning0 = true;
    }
}
void invokeCircles(){
    if (!threadRunning1) {
        int iret = pthread_create( &thread1, NULL, RunCircles, NULL);
        pthread_mutex_init(&mutex1, NULL);
        threadRunning1 = true;
    }
}
void joinThread0(){
    pthread_join(thread0,NULL);
    threadRunning0 = false;
}
void joinThread1(){
    pthread_join(thread1,NULL);
    threadRunning1 = false;
}
void exitThread(){
    pthread_exit(NULL);
}
void killThread0(){
    if (threadRunning0) {
        int iret = pthread_kill(thread0, SIGTERM);
    }
}
void killThread1(){
    if (threadRunning1) {
        int iret = pthread_kill(thread1, SIGTERM);
    }
}
void lockThread0(){
    pthread_mutex_lock(&mutex0);
}
void unlockThread0(){
    pthread_mutex_unlock(&mutex0);
}
void lockThread1(){
    pthread_mutex_lock(&mutex1);
}
void unlockThread1(){
    pthread_mutex_unlock(&mutex1);
}

The test program draws lines in one thread and circles in another thread. You start and stop the threads by pressing '1' and '2' and 'ESC' quits.


Code: (Select All)
'***********************************************************************************
' Proof of concept threading in QB64pe.
' by justsomeguy
'***********************************************************************************

' Thread Library Declaration
DECLARE LIBRARY "./pthreadGFXTest"
  SUB invokeLines ' start Lines thread
  SUB invokeCircles ' start Circles thread
  SUB joinThread0 ' wait til thread is finished
  SUB joinThread1 ' wait til thread is finished
  SUB exitThread ' must be called as thread exits
  SUB killThread0 ' kill the thread
  SUB killThread1 ' kill the thread
  SUB lockThread0 ' mutex lock
  SUB unlockThread0 ' mutex unlock
  SUB lockThread1 ' mutex lock
  SUB unlockThread1 ' mutex unlock
END DECLARE

' Global variables
DIM SHARED AS INTEGER q0, q1 ' quit signals
DIM AS STRING ky

' Setup screen
_TITLE "Thread test"
SCREEN _NEWIMAGE(1024, 768, 32)
_FONT 8
CLS

' Fire up freerunning threads
invokeCircles
invokeLines

' Campout in an infinite loop
DO
  ky = INKEY$
  LOCATE 1, 1
  PRINT "Lines are drawn on one thread, Circles are drawn in a second thread."
  PRINT "Press '1' to toggle the Line drawing thread. "
  PRINT "Press '2' to toggle the Circle drawing thread."
  PRINT "Press 'ESC' to exit."

  IF ky = "1" THEN
    q0 = NOT q0
    IF q0 THEN
      joinThread0
    ELSE
      invokeLines
    END IF
  END IF

  IF ky = "2" THEN
    q1 = NOT q1
    IF q1 THEN
      joinThread1
    ELSE
      invokeCircles
    END IF
  END IF

  ' Quit the whole program
  IF ky = CHR$(27) THEN q0 = -1: q1 = -1: joinThread0: joinThread1: SYSTEM
LOOP


'***********************************************************************************
' Threaded
'***********************************************************************************

SUB lines ()
  ' Free running loop
  DO
    ' lock a mutex, just to be safe
    lockThread0
    ' Do something
    LINE (RND * _WIDTH, RND * _HEIGHT)-(RND * _WIDTH, RND * _HEIGHT), _RGB32(RND * 255, RND * 255, RND * 255)
    ' unlock mutex
    unlockThread0
    ' do I need to jump out?
  LOOP UNTIL q0 = -1
  ' Must call exitThread when leaving, so that joinThread works.
  exitThread
END SUB

SUB circles ()
  ' Free running loop
  DO
    ' lock a mutex, just to be safe
    lockThread1
    ' Do something
    CIRCLE (RND * _WIDTH, RND * _HEIGHT), RND * 50, _RGB32(RND * 255, RND * 255, RND * 255)
    ' unlock mutex
    unlockThread1
    ' do I need to jump out?
  LOOP UNTIL q1 = -1
  ' Must call exitThread when leaving, so that joinThread works.
  exitThread
END SUB


My goal, is to get my 2d physics engine to reside in a separate thread and have it free running computing collisions and motion while the main thread handles I/O and other logic.

I'm using QB64pe 3.11.0. I tested this on Linux Mint, MacOS and Windows 10. On windows I had to add '-pthread' to compiler settings and '--static' to the linker settings.

[Image: win-Comp-Setting.png]

To get this running on your computer, copy the header to your favorite text editor and save the file under 'pthreadGFXTest.h' Then copy the source to the same directory as the header and make sure your compiler and linker settings are correct. I'm not sure if its necessary, but I save my EXE to the source folder.

Beware that if you decide try playing around with the code, that it could crash in some wild ways. Error messages will not make sense, and it might run a bit and lockup for unknown reasons. QB64pe is not meant to be run like this, so there will not be much help if you try.
Reply
#2
This is a nice little demo, but I would caution you that the QB64 runtime is designed around the QB64 code only running on a single thread. It _will_ blow up if you attempt to do anything non-trivial, there's nothing you can do about it. Feel free to still play around with this, but I just want to be honest with you before you spend more time on this, practically speaking this is a dead-end.
Reply
#3
I am not sure how your way work for you and don't have time to investigate
but on Windows is a way to use win api call ..like is explained on this site

https://aljensencprogramming.wordpress.c...win32-api/

I just tried this threading once in freeBasic and i was not happy with results.
Reply
#4
Quote:I would caution you that the QB64 runtime is designed around the QB64 code only running on a single thread. It _will_ blow up if you attempt to do anything non-trivial, there's nothing you can do about it. Feel free to still play around with this, but I just want to be honest with you before you spend more time on this, practically speaking this is a dead-end.
I'm very well aware that QB64 is not designed for this. I stated this in the last paragraph. In my numerous failed attempts, I'm beginning to learn where the boundaries are. Once, I understand the boundaries, I can write my code accordingly.
Quote:I am not sure how your way work for you and don't have time to investigate
but on Windows is a way to use win api call ..like is explained on this site

https://aljensencprogramming.wordpress.c...win32-api/

I just tried this threading once in freeBasic and i was not happy with results.
I'm using pthreads which is built into QB64, in all of the platforms. So, I avoid anything that would exclude one platform or another. I personally use Linux for 99% of everything, but I understand that I'm a minority. 

My first "working" threading attempts were disappointing, but I began to understand my implementation was wrong.
Reply
#5
(05-27-2024, 02:32 PM)justsomeguy Wrote: I'm very well aware that QB64 is not designed for this. I stated this in the last paragraph. In my numerous failed attempts, I'm beginning to learn where the boundaries are. Once, I understand the boundaries, I can write my code accordingly.
Right, I'm just saying that I know how the QB64 internals work and so I know many of the limitations you're going to hit, and they're so limiting that you basically won't be able to do anything:

1. You cannot call any subs or functions from the threads
2. The threads cannot have any local variables
3. The threads cannot use any strings
4. The threads cannot redim any arrays
5. You cannot start the threads from anywhere except the main code.
6. You cannot use the bulk of the built-in commands
7. You cannot have any errors happen, no `ON KEY` entries, no timers, etc.

Basically all that leaves you able to do is interact with shared non-String variables. That's not a whole lot, and you still have some broken aspects that are not solvable.

The other big reason I'd not recommend it is that the "boundaries" are tied to specific versions of QB64. We make significant changes to the runtime in many releases which change what you can or cannot do in your threads, you're going to end up with code that works in one release and not the next.
Reply
#6
So, write the entire thread callable code in C/C++, avoid any calls to the QB64 runtime, and use shared memory that does not move in memory to exchange data.

This approach seems to defeat the purpose of using BASIC. Still, it might be useful in some niche cases.

This gives me some weekend ideas.  Big Grin
Reply
#7
(05-27-2024, 06:26 PM)a740g Wrote: So, write the entire thread callable code in C/C++, avoid any calls to the QB64 runtime, and use shared memory that does not move in memory to exchange data.

This approach seems to defeat the purpose of using BASIC. Still, it might be useful in some niche cases.

This gives me some weekend ideas.  Big Grin
Yeah pretty much, the reliable option here is to write all the code running in the thread in a different language like C++. You can expose mutexes and lock/unlock functions for them to the QB64 code to protect memory that's being shared.
Reply
#8
Hello all,

I have another test program that uses threads. This time it creates from 1 to 8 worker threads and they draw colored squares across the screen.

As before it needs a header file to setup threading. Its name is 'workers.h'

Code: (Select All)
// workers.h
// Threading Header
#include "pthread.h"
// Only needed for the SIGTERM Constant
#include <signal.h>
#include <unistd.h>
#include <time.h>

#define RETRYCOUNT 5
// Initialize Threads
static pthread_t thread[9];

typedef struct thread_data {
   int id;
} thread_data;

// Easy was to determine if a thread is running
bool threadRunning[] = {false,false,false,false,false,false,false,false,false,false};

// Setup Mutexes.
static pthread_mutex_t mutex[9];

// QB's names for the threaded Subs
// You can locate these in your ''qb64pe/internal/temp'' folder.
// I found these in the 'main.txt'
void SUB_WORKERTHREAD(int32*_SUB_WORKERTHREAD_LONG_ID);

// wrap the subs so that you can easily get the void* for pthread
void* RunWorker(void *arg){
    thread_data *tdata=(thread_data *)arg;
    int a = tdata->id;
    SUB_WORKERTHREAD((int32*)&a);
}

// These are the commands that are accessed by you program
void invokeWorker(int id){
    thread_data tdata;
    tdata.id = id;
    int icount = 0;
    if (!threadRunning[id]) {
        while(pthread_create( &thread[id], NULL, RunWorker, (void *)&tdata))
        {
            icount++;
            if (icount > RETRYCOUNT){
                return;
            }
            sleep(1);
        };
        pthread_mutex_init(&mutex[id], NULL);
        threadRunning[id] = true;
    }
    sleep(1);
}

void joinThread(int id){
    pthread_join(thread[id],NULL);
    threadRunning[id] = false;
}

void exitThread(){
    pthread_exit(NULL);
}

void killThread(int id){
    if (threadRunning[id]) {
        int iret = pthread_kill(thread[id], SIGTERM);
    }
}

void lockThread(int id){
    pthread_mutex_lock(&mutex[id]);
}

void unlockThread(int id){
    pthread_mutex_unlock(&mutex[id]);
}
And the QB side is as follows. Its name is whatever you like.
Code: (Select All)
DECLARE LIBRARY "./workers"
  SUB invokeWorker (BYVAL id AS LONG) ' start Lines thread
  SUB joinThread (BYVAL id AS LONG) ' wait til thread is finished
  SUB exitThread ' must be called as thread exits
  SUB killThread (BYVAL id AS LONG) ' kill the thread
  SUB lockThread (BYVAL id AS LONG) ' mutex lock
  SUB unlockThread (BYVAL id AS LONG) ' mutex unlock
END DECLARE

TYPE tWorker
  xstart AS LONG
  ystart AS LONG
  xsize AS LONG
  ysize AS LONG
  xdir AS _BYTE
  x AS LONG
  y AS LONG
  colour AS LONG
  command AS LONG
  sc AS LONG
  img AS _MEM
  offset AS _OFFSET
  dly AS _BYTE
END TYPE

DIM indx AS LONG
DIM ky AS STRING
DIM AS INTEGER tc

_TITLE "Worker Thread Test"

SCREEN _NEWIMAGE(800, 600, 32)
DO
  CLS
  PRINT "How many worker threads? Low spec machines should start less than 4."
  INPUT "(1 - 8, 0 to quit):"; tc
  IF tc = 0 THEN SYSTEM
LOOP UNTIL tc >= 1 AND tc <= 8
tc = tc - 1
CLS

DIM SHARED AS tWorker worker(tc)

FOR indx = 0 TO tc
  resetWorker indx
  worker(indx).sc = _NEWIMAGE(worker(indx).xsize + 1, worker(indx).ysize + 1, 32)
  worker(indx).img = _MEMIMAGE(worker(indx).sc)
NEXT
LOCATE 1
FOR indx = 0 TO tc
  PRINT "Starting thread:"; indx
  invokeWorker indx
NEXT
CLS
DO
  ky = INKEY$
  IF ky = "d" THEN
    FOR indx = 0 TO tc
      worker(indx).dly = NOT worker(indx).dly
    NEXT
  END IF

  IF ky = "s" THEN
    CLS
    FOR indx = 0 TO tc
      worker(indx).command = NOT worker(indx).command
      IF worker(indx).command THEN
        joinThread indx
      ELSE
        PRINT "Restarting thread:"; indx
        _DISPLAY
        invokeWorker indx
      END IF
    NEXT
    CLS
  END IF

  IF ky = CHR$(27) THEN
    FOR indx = 0 TO tc
      worker(indx).command = -1
      joinThread indx
      SYSTEM
    NEXT
  END IF

  FOR indx = 0 TO tc
    _PRINTSTRING (indx * 100 + 10, 220), "Worker:" + STR$(indx)
    _PUTIMAGE (indx * 100, 250), worker(indx).sc
  NEXT

  _PRINTSTRING (10, 380), "Press 'd' toggle thread slowdown. Press 's' to start/stop threads. Press 'ESC' to exit."
  _LIMIT 60
  _DISPLAY
LOOP

SUB resetWorker (id AS LONG)
  worker(id).xsize = 100
  worker(id).ysize = 100
  worker(id).xstart = 0
  worker(id).ystart = 0
  worker(id).x = worker(id).xstart
  worker(id).y = worker(id).ystart
  worker(id).xsize = 100
  worker(id).ysize = 100
  worker(id).xdir = 1
  worker(id).colour = _RGB32(RND * 255, RND * 255, RND * 255)
END SUB

SUB workerThread (id AS LONG)
  DIM AS INTEGER dly

  DO
    lockThread id
    IF worker(id).x + worker(id).xdir > worker(id).xstart + worker(id).xsize OR worker(id).x + worker(id).xdir < worker(id).xstart THEN
      worker(id).xdir = -worker(id).xdir
      IF worker(id).y + 1 > worker(id).ysize THEN
        resetWorker id
      ELSE
        worker(id).y = worker(id).y + 1
      END IF
    ELSE
      worker(id).x = worker(id).x + worker(id).xdir
    END IF

    worker(id).offset = worker(id).img.OFFSET + (worker(id).x * 4) + (worker(id).y * worker(id).xsize * 4)
    _MEMPUT worker(id).img, worker(id).offset + 0, _BLUE32(worker(id).colour) AS _UNSIGNED _BYTE
    _MEMPUT worker(id).img, worker(id).offset + 1, _GREEN32(worker(id).colour) AS _UNSIGNED _BYTE
    _MEMPUT worker(id).img, worker(id).offset + 2, _RED32(worker(id).colour) AS _UNSIGNED _BYTE
    _MEMPUT worker(id).img, worker(id).offset + 3, 255 AS _UNSIGNED _BYTE

    IF NOT worker(id).dly THEN dly = 0: DO: dly = dly + 1: LOOP UNTIL dly < 0
    unlockThread id

  LOOP WHILE worker(id).command = 0
  exitThread
END SUB
Reply
#9
@justsomeguy just some notes from looking at your code:

1. Don't pass `tdata` on the stack to the thread. You should use  something like `malloc()` to allocate it because you don't know when the other thread will actually start using it, it could happen after invokeWorker returns.
2. Your mutexes aren't doing anything because each thread has its own. You should also initialize them before starting the thread.
3. Because there's no lock protecting access to the images, the main thread could end up reading from the image while a worker thread has only written half a color.
4. I wouldn't declare the`dly` integer inside the SUB.
5. Calling `resetWorker` from the worker thread is not safe.

I would also recommend putting the worker thread `SUB` inside of a `$CHECKING:OFF` block, that will prevent QB64 from inserting the error handling logic into the code for your thread.
Reply
#10
@DSMan195276

Thank you for you suggestions.
Quote:1. Don't pass `tdata` on the stack to the thread. You should use  something like `malloc()` to allocate it because you don't know when the other thread will actually start using it, it could happen after invokeWorker returns.
I would appreciate it if you go into more detail on this. Perhaps some sample code?

Quote:2. Your mutexes aren't doing anything because each thread has its own. You should also initialize them before starting the thread.
I thought this was the case, so commented them out and had same results.

Quote:3. Because there's no lock protecting access to the images, the main thread could end up reading from the image while a worker thread has only written half a color.
This was by design. Early prototypes had corruption of data across the color bands i.e. Threads were writing to the wrong color band.

Quote:4. I wouldn't declare the`dly` integer inside the SUB.
5. Calling `resetWorker` from the worker thread is not safe.
These are me testing the boundaries. Clearly, in production code, I would go different route, like writing it in a different language.
Reply




Users browsing this thread: 16 Guest(s)