QB64 Phoenix Edition
Mandelbrot (Threaded) - Printable Version

+- QB64 Phoenix Edition (https://qb64phoenix.com/forum)
+-- Forum: QB64 Rising (https://qb64phoenix.com/forum/forumdisplay.php?fid=1)
+--- Forum: Code and Stuff (https://qb64phoenix.com/forum/forumdisplay.php?fid=3)
+---- Forum: Programs (https://qb64phoenix.com/forum/forumdisplay.php?fid=7)
+---- Thread: Mandelbrot (Threaded) (/showthread.php?tid=2764)



Mandelbrot (Threaded) - justsomeguy - 06-01-2024

As I mentioned in my other threading post, I was going to attempt to Mandelbrot set in QB64 threaded. 

Before anyone says that Mandelbrot doesn't need to be threaded and one thread is fast enough, you are correct. This is my journey learning about threads and how to actually use them. I wanted share some of what I learned and document some of it. So, if any of you other brave souls attempt this, you have a place to start. That is all for the TED talk.

I used a modified version of qbguy's implementation of Mandelbrot for my basis. That can be found in QB64-PE samples that Steve posted some time ago.https://qb64phoenix.com/forum/showthread.php?tid=769&highlight=qb64-pe+samples

It needs two files to work. First is a header file named "mandelThread.h" The second is the actual basic program. You can name it "Mandelthread.bas" or whatever you like.

I've tried this on Linux, MacOS, and Windows. Windows needs a special consideration.  Under Options --> Compiler Settings... --> C++ Compiler
Flags --> -pthread  and under C++ Linker Flags --> --static.

[Image: Screenshot-from-2024-06-01-16-44-19.png]

There are a few things to tinker with.
Code: (Select All)
'Are outside functions in worker thread allowed?
$LET THREADFUNC = TRUE
'Can the outside function have local variables?
$LET THREADFUNCLOCALVAR = TRUE
' Number of threads (1-32)
CONST cTHREADCOUNT = 32
' These affect actual calculation
CONST cREAL# = -2.0
CONST cIMAG# = -2.0
CONST cINCR# = 0.005

The header file. 

Code: (Select All)
// mandelThread.h
// Threading Header
#include "pthread.h"
// needed the sleep()
#include <unistd.h>
// needed for clock()
#include <time.h>

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

// Setup Mutexes
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// thread arguments
typedef struct thread_data {
   int id;
} thread_data;

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

// 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_WORKER(int32*_SUB_WORKER_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 id = tdata->id;
    threadRunning[id] = true;
    SUB_WORKER((int32*)&id);
}

// These are the commands that are accessed by you program
int invokeWorker(int id){
    // setup arguments to be sent to the thread
    thread_data tdata;
    tdata.id = id;
    // Threads are not always successfully started
    // So, we retry until it sticks or we exceed the retry count.
    int retry_count = 0;
    // Is it already running?
    if (!threadRunning[id]) {
        // try to start thread
        while(pthread_create( &thread[id], NULL, RunWorker, (void *)&tdata))
        {
            // thread is not running
            if (retry_count++ > RETRYCOUNT){
                // jumpout if tried more than RETRYCOUNT
                return threadRunning[id];
            }
            // wait a sec before trying again
            sleep(1);
        };
        // Give thread a sec to spool up
        int timeout_time = clock() * 1000 / CLOCKS_PER_SEC + 1000;
        do {} while (clock() * 1000 / CLOCKS_PER_SEC <= timeout_time && threadRunning[id] != true);
    }
    return threadRunning[id];
}


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

void exitThread(){
    pthread_exit(NULL);
}

void lockThread(){
    pthread_mutex_lock(&mutex);
}

void tryLockThread(){
    while (pthread_mutex_trylock(&mutex) != 0) {};
}

void unlockThread(){
    pthread_mutex_unlock(&mutex);
}

void lockDestroy(){
    pthread_mutex_destroy(&mutex);
}
The QB file.

Code: (Select All)
' Header interface
$CHECKING:OFF
DECLARE LIBRARY "./mandelThread"
  FUNCTION invokeWorker (BYVAL id AS LONG) ' start worker thread
  SUB joinThread (BYVAL id AS LONG) ' wait til thread is finished
  SUB exitThread ' must be called as thread exits
  SUB lockThread ' mutex lock
  SUB unlockThread ' mutex unlock
  SUB tryLockThread ' keep trying to lock thread
  SUB lockDestroy
END DECLARE
'Are outside functions in worker thread allowed?
$LET THREADFUNC = TRUE
'Can the outside function have local variables?
$LET THREADFUNCLOCALVAR = TRUE

_TITLE "Threaded Mandelbrot"
SCREEN _NEWIMAGE(800, 800, 32)
' Number of threads (1-32)
CONST cTHREADCOUNT = 32

' These affect actual calculation
CONST cREAL# = -2.0
CONST cIMAG# = -2.0
CONST cINCR# = 0.005

TYPE tWORKER
  xStart AS LONG
  yStart AS LONG
  xSize AS LONG
  ySize AS LONG
  xEnd AS LONG
  yEnd AS LONG
  x AS LONG
  y AS LONG
  r AS DOUBLE
  xt AS DOUBLE
  yt AS DOUBLE
  xx AS DOUBLE
  yy AS DOUBLE
  real AS DOUBLE
  imag AS DOUBLE
  incr AS DOUBLE
  colour AS DOUBLE
  sc AS LONG
  img AS _MEM
  offset AS _OFFSET
  command AS _BYTE
END TYPE

'Initialize resources
DIM SHARED AS tWORKER mWorker(cTHREADCOUNT)
DIM SHARED AS LONG argId
DIM AS LONG indx
DIM ky AS STRING

'Setup workers initial values
FOR indx = 0 TO cTHREADCOUNT - 1
  initWorker indx
NEXT

'Start the workers
FOR indx = 0 TO cTHREADCOUNT - 1
  IF invokeWorker(indx) = 0 THEN PRINT "Failed to start worker thread!": END
NEXT

' Main loop
DO
  ' handle user input
  ky = INKEY$
  IF ky = CHR$(27) THEN
    FOR indx = 0 TO cTHREADCOUNT - 1
      'set the workers command to quit
      mWorker(indx).command = -1
      ' wait for the thread to quit
      joinThread id
    NEXT
    ' cleanup the lock
    lockDestroy
    SYSTEM
  END IF
  ' display workers work
  FOR indx = 0 TO cTHREADCOUNT - 1
    ' lock the images so that they can be displayed
    ' in the case of an image it may not be strictly necessary, but
    ' data that needs to be correct it does.
    lockThread
    _PUTIMAGE (mWorker(indx).xStart, mWorker(indx).yStart), mWorker(indx).sc
    unlockThread
  NEXT
  ' keeping the framerate slow so that workers have more access to the image.
  _LIMIT 5
  _DISPLAY
LOOP
SYSTEM

'Work is split amongst the threads in horizontal bands.
SUB initWorker (id AS LONG)
  mWorker(id).xSize = _WIDTH
  mWorker(id).ySize = _HEIGHT / cTHREADCOUNT
  mWorker(id).xStart = 0
  mWorker(id).yStart = id * mWorker(id).ySize
  mWorker(id).xEnd = mWorker(id).xStart + mWorker(id).xSize
  mWorker(id).yEnd = mWorker(id).yStart + mWorker(id).ySize
  mWorker(id).incr = cINCR
  mWorker(id).real = cREAL
  mWorker(id).imag = cIMAG + mWorker(id).yStart * mWorker(id).incr

  mWorker(id).sc = _NEWIMAGE(mWorker(id).xEnd, mWorker(id).yEnd, 32)
  mWorker(id).img = _MEMIMAGE(mWorker(id).sc)
END SUB

'###################################################################################################
' Threaded Subs
'###################################################################################################
SUB worker (id AS LONG)
  ' Worker will continue to loop until mWorker(id).command is true
  DO
    mWorker(id).y = 0: DO
      mWorker(id).r = mWorker(id).real
      mWorker(id).x = 0: DO
        ' Decide on how the thread is going to run
        ' THREADFUNC determines if the calculation of the Mandelbrot will use a function or not
        ' THREADFUNCLOCALVAR determines if the function uses local variables
        $IF THREADFUNC = TRUE THEN
          lockThread
          argId = id
          $IF THREADFUNCLOCALVAR = TRUE THEN
            mandelA
          $ELSE
              mandelT
          $END IF
          unlockThread
        $ELSE
            mWorker(id).xt = mWorker(id).r: mWorker(id).yt = mWorker(id).imag
            mWorker(id).colour = 256.0: Do
            mWorker(id).xx = mWorker(id).xt * mWorker(id).xt: mWorker(id).yy = mWorker(id).yt * mWorker(id).yt
            If mWorker(id).xx + mWorker(id).yy >= 4 Then Exit Do
            mWorker(id).yt = mWorker(id).xt * mWorker(id).yt * 2 + mWorker(id).imag
            mWorker(id).xt = mWorker(id).xx - mWorker(id).yy + mWorker(id).r
            mWorker(id).colour = mWorker(id).colour - 1: Loop While mWorker(id).colour > 1
        $END IF
        ' calculate offset based on the x and y cooridates
        mWorker(id).offset = mWorker(id).img.OFFSET + (mWorker(id).x * 4) + (mWorker(id).y * mWorker(id).xSize * 4)
        lockThread
        ' draw colors to the image
        _MEMPUT mWorker(id).img, mWorker(id).offset + 0, mWorker(id).colour AS _UNSIGNED _BYTE
        _MEMPUT mWorker(id).img, mWorker(id).offset + 1, mWorker(id).colour AS _UNSIGNED _BYTE
        _MEMPUT mWorker(id).img, mWorker(id).offset + 2, mWorker(id).colour AS _UNSIGNED _BYTE
        _MEMPUT mWorker(id).img, mWorker(id).offset + 3, 255 AS _UNSIGNED _BYTE
        unlockThread
        mWorker(id).r = mWorker(id).r + mWorker(id).incr
      mWorker(id).x = mWorker(id).x + 1: LOOP UNTIL mWorker(id).x >= mWorker(id).xEnd
      mWorker(id).imag = mWorker(id).imag + mWorker(id).incr
    mWorker(id).y = mWorker(id).y + 1: LOOP UNTIL mWorker(id).y >= mWorker(id).yEnd

    'reset worker back to its original position
    mWorker(id).real = cREAL
    mWorker(id).imag = cIMAG + mWorker(id).yStart * mWorker(id).incr

  LOOP UNTIL mWorker(id).command
  ' This allows the thread to be joined at the end
  exitThread
END SUB
'###################################################################################################
' Threaded helper subs
'###################################################################################################
' Mandelbrot with no local variables
SUB mandelT
  mWorker(argId).xt = mWorker(argId).r: mWorker(argId).yt = mWorker(argId).imag
  mWorker(argId).colour = 256.0: DO
    mWorker(argId).xx = mWorker(argId).xt * mWorker(argId).xt: mWorker(argId).yy = mWorker(argId).yt * mWorker(argId).yt
    IF mWorker(argId).xx + mWorker(argId).yy >= 4 THEN EXIT DO
    mWorker(argId).yt = mWorker(argId).xt * mWorker(argId).yt * 2 + mWorker(argId).imag
    mWorker(argId).xt = mWorker(argId).xx - mWorker(argId).yy + mWorker(argId).r
  mWorker(argId).colour = mWorker(argId).colour - 1: LOOP WHILE mWorker(argId).colour > 1
END SUB

' Mandelbrot with local variables
SUB mandelA
  DIM AS DOUBLE x, y, xx, yy, c
  x = mWorker(argId).r: y = mWorker(argId).imag
  FOR c = 256.0 TO 1 STEP -1
    xx = x * x: yy = y * y
    IF xx + yy >= 4 THEN EXIT FOR
    y = x * y * 2 + mWorker(argId).imag
    x = xx - yy + mWorker(argId).r
  NEXT
  mWorker(argId).colour = c
END SUB


[Image: Screenshot-from-2024-06-01-17-14-24.png]