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.
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
|