Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Threading in QB64pe (again)
#11
Quote:
Quote: Wrote: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?
This is an example of malloc() and realloc(). The malloc() function provides memory, the realloc() function can expand this memory to the desired size if necessary.
Any input other than a number ends the input. -- malloc() + realloc()

Code: (Select All)

//Benutzung von malloc() und realloc() - 28. Mai 2024

#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>

int main(void)
{
int *i_zgr;
int zaehler = 0, summe;
size_t bytes = sizeof(int);

//Zunächst nur Platz für einen Wert
if((i_zgr = malloc(bytes)) == NULL)
{
printf("\nMalloc() fehlgeschlagen!");
exit(1);
}
printf("\nMehrere Zahlen eingeben fuer ein temporaeres Feld.");
printf("\nKeine Zahl bedeutet Ende der Eingabe.\n\n");

while(scanf("%d", &i_zgr[zaehler]) == 1)
{
++zaehler;
//Erstelltes Feld erweitern
if((i_zgr = realloc(i_zgr, bytes * (zaehler + 1))) == NULL)
{
printf("\n\nErweiterung fehlgeschlagen!");
exit(1);
}
}
summe = 0;
printf("\nSie haben eingegeben:\n");
for(int i = 0; i < zaehler; i++)
{
printf("Zeiger[%2d] = %3d\n", i, i_zgr[i]);
summe += i_zgr[i];
}
printf("\n\nSumme der eingegebenen Zahlen: %3d\n", summe);

//Speicher wieder freigeben
free(i_zgr);

return(0);
}

[Image: malloc-realloc2024-05-28-204243.jpg]
Reply
#12
(05-28-2024, 04:03 PM)justsomeguy Wrote: I would appreciate it if you go into more detail on this. Perhaps some sample code?
I would recommend reading up on stack vs. heap allocation in C and C++. That said the gist is that `thread_data tdata` declares the `thread_data` structure on the stack, that means it only exists while the main thread is in the call to `invokeWorker`. Once `invokeWorker` returns, the thread_data variable goes away and its memory will be reused for something else.

The issue that comes up is that a pointer to the `tdata` variable is passed to `pthread_create()` (as the parameter to the new thread). That starts a separate thread and the original thread can can get the success result from `pthread_create` and then return from `invokeWorker` all before the new thread actually starts running any code. Then you're in the situation where the new thread will be reading from the `tdata` variable while it is already gone from the original thread (and its memory is likely being reused). It's a situation where the issue is probably avoided in this simplified example, but will start to blow up once your code gets more complex and you're calling more functions.

(05-28-2024, 04:03 PM)justsomeguy Wrote:
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.
Maybe we're talking about the same thing, but I'm talking about the issue that your code writes four bytes (via four _MEMPUT commands) to the image. The main thread may read the image data while you have only written two of the four bytes, displaying an incorrect color as the result. A mutex is used to avoid this kind of issue, you give each image a mutex and then lock/unlock the mutex every time the image is used (so, around the `_MEMPUT`s and the `_PUTIMAGE`). That way the main thread will only read from an image when the corresponding worker thread is not writing to it.
Reply
#13
I have updated the "Worker demo" based upon some of DSMan195276's suggestions.

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

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

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

// Easy was to determine if a thread is running
static 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 id = tdata->id;
    threadRunning[id] = true;
    SUB_WORKERTHREAD((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 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]);
}

void initLock(int id)
{
    pthread_mutex_init(&mutex[id], NULL);
}
Code: (Select All)

'****************************************************************************
' Worker Test
' by justsomeguy
'****************************************************************************

' Header interface
DECLARE LIBRARY "./workers"
  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 killThread (BYVAL id AS LONG) ' kill the thread
  SUB initLock (BYVAL id AS LONG) ' initialize mutex
  SUB lockThread (BYVAL id AS LONG) ' mutex lock
  SUB unlockThread (BYVAL id AS LONG) ' mutex unlock
END DECLARE

' UDT type definition
' this is going to be shared with the workers.
TYPE tWorker
  id AS _BYTE
  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
  dlyCount AS INTEGER
END TYPE

' Local Variables
DIM AS LONG indx
DIM AS STRING ky
DIM AS INTEGER tc

' Setup Screen
_TITLE "Worker Thread Test"
SCREEN _NEWIMAGE(800, 600, 32)

' Poll user on how many worker threads
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

' Initialize our worker array
DIM SHARED AS tWorker worker(tc)

' Setup initial values for each of the workers
FOR indx = 0 TO tc
  initWorker indx
  ' These are the actual screens that the workers output to.
  worker(indx).sc = _NEWIMAGE(worker(indx).xsize + 1, worker(indx).ysize + 1, 32)
  worker(indx).img = _MEMIMAGE(worker(indx).sc)
NEXT

LOCATE 1
' Start workers
FOR indx = 0 TO tc
  PRINT "Starting thread:"; indx
  IF invokeWorker(indx) = 0 THEN PRINT "Failed to start worker thread!": END
NEXT
CLS

'main loop
DO
  ky = INKEY$
  ' toggle the built in delay for the worker
  IF ky = "d" THEN
    FOR indx = 0 TO tc
      worker(indx).dly = NOT worker(indx).dly
    NEXT
  END IF
  ' start/stop the workers
  IF ky = "s" THEN
    CLS
    FOR indx = 0 TO tc
      ' toggle worker command
      ' this could be more elaborate adding more functionality
      worker(indx).command = NOT worker(indx).command

      'if worker command is -1 then worker is going to quit
      IF worker(indx).command THEN
        ' if worker quits then wait until all the threads quit
        joinThread indx
      ELSE
        ' restart threads when command is 0
        PRINT "Restarting thread:"; indx
        IF invokeWorker(indx) = 0 THEN PRINT "Failed to start worker thread!": END
        _DISPLAY
      END IF
    NEXT
    CLS
  END IF
  ' stop the workers and quit
  IF ky = CHR$(27) THEN
    FOR indx = 0 TO tc
      worker(indx).command = -1
      joinThread indx
      SYSTEM
    NEXT
  END IF

  'Draw the results of the work
  FOR indx = 0 TO tc
    ' display the id that was passed to worker
    _PRINTSTRING (indx * 100 + 10, 220), "Worker:" + STR$(worker(indx).id)
    ' lock image to keep worker from writing to it while
    ' QB64 is drawing it to the screen.
    ' suggested by DSMan195276
    lockThread indx
    _PUTIMAGE (indx * 100, 250), worker(indx).sc
    unlockThread indx
  NEXT
  ' on screen instructions
  _PRINTSTRING (10, 380), "Press 'd' toggle thread slowdown. Press 's' to start/stop threads. Press 'ESC' to exit."
  _LIMIT 60
  _DISPLAY
LOOP

' Initialize worker
SUB initWorker (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

' Threaded Worker
SUB workerThread (id AS LONG)
  ' turn checking off because it would useless in the context of a thread
  ' suggested by DSMan195276
  $CHECKING:OFF
  initLock id
  worker(id).id = id ' this is silly, but it is to test if 'id' get passed successfully.
  DO
    ' This worker fills in a square by snaking side to side
    ' when it reaches bottom it changes color and returns to the top.
    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
        worker(id).x = worker(id).xstart
        worker(id).y = worker(id).ystart
        worker(id).xdir = 1
        worker(id).colour = _RGB32(RND * 255, RND * 255, RND * 255)
      ELSE
        worker(id).y = worker(id).y + 1
      END IF
    ELSE
      worker(id).x = worker(id).x + worker(id).xdir
    END IF
    ' draw pixels directly to the image
    worker(id).offset = worker(id).img.OFFSET + (worker(id).x * 4) + (worker(id).y * worker(id).xsize * 4)
    ' lock image to keep QB64 from trying to draw it while the worker is writing to it
    ' suggested by DSMan195276
    lockThread id
    _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
    ' unlock after done
    unlockThread id
    ' Put a small delay so that you can see the worker snake
    IF NOT worker(id).dly THEN worker(id).dlyCount = 0: DO: worker(id).dlyCount = worker(id).dlyCount + 1: LOOP UNTIL worker(id).dlyCount < 0
  LOOP WHILE worker(id).command = 0
  exitThread
  $CHECKING:ON
END SUB
Quote:I would recommend reading up on stack vs. heap allocation in C and C++. That said the gist is that `thread_data tdata` declares the `thread_data` structure on the stack, that means it only exists while the main thread is in the call to `invokeWorker`. Once `invokeWorker` returns, the thread_data variable goes away and its memory will be reused for something else.

The issue that comes up is that a pointer to the `tdata` variable is passed to `pthread_create()` (as the parameter to the new thread). That starts a separate thread and the original thread can can get the success result from `pthread_create` and then return from `invokeWorker` all before the new thread actually starts running any code. Then you're in the situation where the new thread will be reading from the `tdata` variable while it is already gone from the original thread (and its memory is likely being reused). It's a situation where the issue is probably avoided in this simplified example, but will start to blow up once your code gets more complex and you're calling more functions.
It looks like worked around this problem by adding a 1 second sleep timer after starting the thread. This gave time for the thread to spool up.

In this latest version, it checks if the thread is started before 'invokeWorker' returns. 'invokeWorker' is now a function that returns a success or fail based on if the thread started. Now the threads start a lot faster.

I double check if the arguments going to the worker by recording the 'id' to its UDT. I then print that result above each worker. So far they have all been aligned. 

I have been reading and I'm seeing a few examples of using malloc to pass arguments to the thread. I'm pondering the easiest and simplest way to implement it.

Quote:Maybe we're talking about the same thing, but I'm talking about the issue that your code writes four bytes (via four _MEMPUT commands) to the image. The main thread may read the image data while you have only written two of the four bytes, displaying an incorrect color as the result. A mutex is used to avoid this kind of issue, you give each image a mutex and then lock/unlock the mutex every time the image is used (so, around the `_MEMPUT`s and the `_PUTIMAGE`). That way the main thread will only read from an image when the corresponding worker thread is not writing to it.
I did as you suggested. I hope I implemented it right. I did notice a bit of a slowdown. I suspect its a race condition that I won't have much control over.

Thank you for your suggestions and keep them coming.
Reply
#14
(05-29-2024, 03:33 PM)justsomeguy Wrote: It looks like worked around this problem by adding a 1 second sleep timer after starting the thread. This gave time for the thread to spool up.

In this latest version, it checks if the thread is started before 'invokeWorker' returns. 'invokeWorker' is now a function that returns a success or fail based on if the thread started. Now the threads start a lot faster.

I double check if the arguments going to the worker by recording the 'id' to its UDT. I then print that result above each worker. So far they have all been aligned. 

I have been reading and I'm seeing a few examples of using malloc to pass arguments to the thread. I'm pondering the easiest and simplest way to implement it.
I would look into using a condition variable rather than a busy wait, but overall your approach is fine and should work.

I would like to point out a bit of a nasty issue when using threading in C++ (and many other languages) - the compiler is free to reorganize your code as long as it works the same in a single threaded view. The result here is that the C++ compiler could place your `threadRunning[id] = true;` line before the ` int id = tdata->id;` line, defeating the protection you're attempting. The compiler is not allowed to reorganize such code around mutex lock/unlock or condition variable statements though, so they offer protection against that. It's very unlikely to cause a noticeable issue here, but figured I'd point the issue out, it's one reason why using the provided synchronization primitives like mutexes and condition variables can be important.

(05-29-2024, 03:33 PM)justsomeguy Wrote: I did as you suggested. I hope I implemented it right. I did notice a bit of a slowdown. I suspect its a race condition that I won't have much control over.

Thank you for your suggestions and keep them coming.
Yep, that looks pretty good. One thing is that I wouldn't call `initLock` inside of the worker sub, just call it before you call `pthread_create()`. The risk here is that the lock is not yet initialized when the main thread attempts to take it the first time.

The slowdown is expected, what you're seeing is more or less the situation I was taking about - the main thread was reading while the `_MEMPUT` commands were happening. Now with a mutex in place, instead of both threads reading/writing at the same time, they have to take turns, and that does slow things down when one thread has to wait for the other to finish.

I will say, a mutex is a bit "heavy" for this case since the writing can happen very often. The lock/unlock operations on a mutex aren't super expensive, but also aren't free, if the worker thread has no delay then it might spend the majority of its time just on locking/unlocking the mutex. That in turn leads to more conflicts with the main thread since the mutex spends a lot of its time locked by the worker thread. There's strategies you can use to reduce the amount of locking/unlocking you actually have to do Ex. batch operations together, or use a rotating list of images to avoid so much locking, etc. and that would get you closer to the previous speed with no locking at all.
Reply
#15
@DSMan195276 First, Thank you for your help! Without your insight this would have been another failed attempt.

Since I'm starting and stopping threads, it seems to me that if I re-initialize the mutex it might cause problems.  So, my solution is to destroy the mutex after they join. Kinda following the example here https://www.thegeekstuff.com/2012/05/c-mutex-examples/

Mutex initialize:
Code: (Select All)
// 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
        pthread_mutex_init(&mutex[id], NULL);
        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];
}
Mutex destroy:
Code: (Select All)
void joinThread(int id){
    pthread_join(thread[id],NULL);
    threadRunning[id] = false;
    pthread_mutex_destroy(&mutex[id]);
}

So, I hope this is the last iteration of this example.

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

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

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

// Easy was to determine if a thread is running
static 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 id = tdata->id;
    threadRunning[id] = true;
    SUB_WORKERTHREAD((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
        pthread_mutex_init(&mutex[id], NULL);
        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;
    pthread_mutex_destroy(&mutex[id]);
}

void exitThread(){
    pthread_exit(NULL);
}

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

void unlockThread(int id){
    pthread_mutex_unlock(&mutex[id]);
}
 
And full QB64
Code: (Select All)
'****************************************************************************
' Worker Test
' by justsomeguy
'****************************************************************************

' Header interface
DECLARE LIBRARY "./workers"
  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 (BYVAL id AS LONG) ' mutex lock
  SUB unlockThread (BYVAL id AS LONG) ' mutex unlock
END DECLARE

' UDT type definition
' this is going to be shared with the workers.
TYPE tWorker
  id AS _BYTE
  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
  dlyCount AS INTEGER
END TYPE

' Local Variables
DIM AS LONG indx
DIM AS STRING ky
DIM AS INTEGER tc

' Setup Screen
_TITLE "Worker Thread Test"
SCREEN _NEWIMAGE(800, 600, 32)

' Poll user on how many worker threads
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

' Initialize our worker array
DIM SHARED AS tWorker worker(tc)

' Setup initial values for each of the workers
FOR indx = 0 TO tc
  initWorker indx
  ' These are the actual screens that the workers output to.
  worker(indx).sc = _NEWIMAGE(worker(indx).xsize + 1, worker(indx).ysize + 1, 32)
  worker(indx).img = _MEMIMAGE(worker(indx).sc)
NEXT

LOCATE 1
' Start workers
FOR indx = 0 TO tc
  PRINT "Starting thread:"; indx
  IF invokeWorker(indx) = 0 THEN PRINT "Failed to start worker thread!": END
NEXT
CLS

'main loop
DO
  ky = INKEY$
  ' toggle the built in delay for the worker
  IF ky = "d" THEN
    FOR indx = 0 TO tc
      worker(indx).dly = NOT worker(indx).dly
    NEXT
  END IF
  ' start/stop the workers
  IF ky = "s" THEN
    CLS
    FOR indx = 0 TO tc
      ' toggle worker command
      ' this could be more elaborate adding more functionality
      worker(indx).command = NOT worker(indx).command
      'if worker command is -1 then worker is going to quit
      IF worker(indx).command THEN
        ' if worker quits then wait until all the threads quit
        joinThread indx
      ELSE
        ' restart threads when command is 0
        PRINT "Restarting thread:"; indx
        IF invokeWorker(indx) = 0 THEN PRINT "Failed to start worker thread!": END
        _DISPLAY
      END IF
    NEXT
    CLS
  END IF
  ' stop the workers and quit
  IF ky = CHR$(27) THEN
    FOR indx = 0 TO tc
      worker(indx).command = -1
      joinThread indx
      SYSTEM
    NEXT
  END IF

  'Draw the results of the work
  FOR indx = 0 TO tc
    ' display the id that was passed to worker
    _PRINTSTRING (indx * 100 + 10, 220), "Worker:" + STR$(worker(indx).id)
    ' lock image to keep worker from writing to it while
    ' QB64 is drawing it to the screen.
    ' suggested by DSMan195276
    lockThread indx
    _PUTIMAGE (indx * 100, 250), worker(indx).sc
    unlockThread indx
  NEXT
  ' on screen instructions
  _PRINTSTRING (10, 380), "Press 'd' toggle thread slowdown. Press 's' to start/stop threads. Press 'ESC' to exit."
  _LIMIT 60
  _DISPLAY
LOOP

' Initialize worker
SUB initWorker (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

' Threaded Worker
SUB workerThread (id AS LONG)
  ' turn checking off because it would useless in the context of a thread
  ' suggested by DSMan195276
  $CHECKING:OFF
  worker(id).id = id ' this is silly, but it is to test if 'id' get passed successfully.
  DO
    ' This worker fills in a square by snaking side to side
    ' when it reaches bottom it changes color and returns to the top.
    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
        worker(id).x = worker(id).xstart
        worker(id).y = worker(id).ystart
        worker(id).xdir = 1
        worker(id).colour = _RGB32(RND * 255, RND * 255, RND * 255)
      ELSE
        worker(id).y = worker(id).y + 1
      END IF
    ELSE
      worker(id).x = worker(id).x + worker(id).xdir
    END IF
    ' draw pixels directly to the image
    worker(id).offset = worker(id).img.OFFSET + (worker(id).x * 4) + (worker(id).y * worker(id).xsize * 4)
    ' lock image to keep QB64 from trying to draw it while the worker is writing to it
    ' suggested by DSMan195276
    lockThread id
    _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
    ' unlock after done
    unlockThread id
    ' Put a small delay so that you can see the worker snake
    IF NOT worker(id).dly THEN worker(id).dlyCount = 0: DO: worker(id).dlyCount = worker(id).dlyCount + 1: LOOP UNTIL worker(id).dlyCount < 0
  LOOP WHILE worker(id).command = 0
  exitThread
  $CHECKING:ON
END SUB

I think I'm going to work up a new example, perhaps Mandelbrot, we will see.
Reply
#16
Code: (Select All)
#include<strsafe.h>
int32 FUNC_MYTHREADFUNCTION(ptrszint*_FUNC_MYTHREADFUNCTION_OFFSET_LPPARAM);
extern "C"{
        __declspec(dllexport) int32 MyThreadFunction(ptrszint*lpParam){
                return FUNC_MYTHREADFUNCTION((lpParam));
        }
}

int32 sizeoftchar(){
        return sizeof(TCHAR);
}
Code: (Select All)
Option _Explicit
$Console:Only

Type MyData
    As Long val1, val2
End Type

Const BUF_SIZE = 255
Const MAX_THREADS = 20
Const HEAP_ZERO_MEMORY = &H00000008
Const INFINITE = 4294967295
Const STD_OUTPUT_HANDLE = -11
Const INVALID_HANDLE_VALUE = -1

Const MB_OK = 0

Const FORMAT_MESSAGE_ALLOCATE_BUFFER = &H00000100
Const FORMAT_MESSAGE_FROM_SYSTEM = &H00001000
Const FORMAT_MESSAGE_IGNORE_INSERTS = &H00000200
Const LANG_NEUTRAL = &H00
Const SUBLANG_DEFAULT = &H01

Const LMEM_ZEROINIT = &H0040

Declare CustomType Library
    Function LoadLibrary%& (lpLibFileName As String)
    Function GetProcAddress%& (ByVal hModule As _Offset, lpProcName As String)
    Function FreeLibrary%% (ByVal hLibModule As _Offset)
    Sub FreeLibrary (ByVal hLibModule As _Offset)
    Function GetLastError& ()
    Function HeapAlloc%& (ByVal hHeap As _Offset, Byval dwFlags As Long, Byval dwBytes As _Offset)
    Function GetProcessHeap%& ()
    Sub ExitProcess (ByVal uExitCode As _Unsigned Long)
    Function CreateThread%& (ByVal lpThreadAttributes As _Offset, Byval dwStackSize As _Offset, Byval lpStartAddress As _Offset, Byval lpParameter As _Offset, Byval dwCreationFlags As Long, Byval lpThreadId As _Offset)
    Function WaitForMultipleObjects& (ByVal nCount As Long, Byval lpHandles As _Offset, Byval bWaitAll As _Byte, Byval dwMilliseconds As Long)
    Sub WaitForMultipleObjects (ByVal nCount As Long, Byval lpHandles As _Offset, Byval bWaitAll As _Byte, Byval dwMilliseconds As Long)
    Function CloseHandle%% (ByVal hObject As _Offset)
    Sub CloseHandle (ByVal hObject As _Offset)
    Function HeapFree%% (ByVal hHeap As _Offset, Byval dwFlags As Long, Byval lpMem As _Offset)
    Sub HeapFree (ByVal hHeap As _Offset, Byval dwFlags As Long, Byval lpMem As _Offset)
    Sub StringCchPrintf Alias "StringCchPrintfA" (ByVal pszDest As _Offset, Byval cchDest As _Offset, byvalpszFormat As String, Byval arg1 As Long, Byval arg2 As Long)
    Sub StringCchPrintf2 Alias "StringCchPrintfA" (ByVal pszDest As _Offset, Byval cchDest As _Offset, pszFormat As String, lpszFunction As String, Byval error As Long, Byval lpMsgBuf As _Offset)
    Sub StringCchLength Alias "StringCchLengthA" (ByVal psz As _Offset, Byval cchMax As _Offset, Byval pcchLength As _Offset)
    Function GetStdHandle%& (ByVal nStdHandle As Long)
    Function CreateMutex%& Alias "CreateMutexA" (ByVal lpMutexAttributes As _Offset, Byval bInitialOwner As Long, Byval lpName As _Offset)
    Sub WriteConsole (ByVal hConsoleOutput As _Offset, Byval lpBuffer As _Offset, Byval nNumberOfCharsToWrite As Long, Byval lpNumberOfCharsWritten As _Offset, Byval lpReserved As _Offset)
    Sub FormatMessage Alias FormatMessageA (ByVal dwFlags As Long, Byval lpSource As Long, Byval dwMessageId As Long, Byval dwLanguageId As Long, Byval lpBuffer As _Offset, Byval nSize As Long, Byval Arguments As _Offset)
    Sub MessageBox Alias "MessageBoxA" (ByVal hWnd As _Offset, Byval lpText As _Offset, lpCaption As String, Byval uType As _Unsigned Long)
    Sub LocalFree (ByVal hMem As _Offset)
    Function LocalAlloc%& (ByVal uFlags As _Unsigned Long, Byval uBytes As _Unsigned _Offset)
    Function lstrlen& Alias "lstrlenA" (ByVal lpString As _Offset)
    Function LocalSize%& (ByVal hMem As _Offset)
    Sub SetLastError (ByVal dwError As Long)
End Declare

Declare Library "threadwin"
    Function sizeoftchar& ()
End Declare

Declare Library
    Function MAKELANGID& (ByVal p As Long, Byval s As Long)
End Declare

Dim As _Offset libload: libload = LoadLibrary(Command$(0))
Dim As _Offset MyThreadFunc: MyThreadFunc = GetProcAddress(libload, "MyThreadFunction")

Dim As MyData pDataArray(1 To MAX_THREADS)
Dim As Long dwThreadIdArray(1 To MAX_THREADS)
Dim As _Offset hThreadArray(1 To MAX_THREADS), heap(1 To MAX_THREADS)

Dim As _Offset ghMutex: ghMutex = CreateMutex(0, 0, 0)
If ghMutex = 0 Then
    ErrorHandler "CreateMutex"
End If
Dim As Long i
For i = 1 To MAX_THREADS
    heap(i) = HeapAlloc(GetProcessHeap, HEAP_ZERO_MEMORY, Len(pDataArray(i)))
    Dim As _MEM pdata: pdata = _MemNew(8)
    _MemPut pdata, pdata.OFFSET, heap(i)
    _MemGet pdata, pdata.OFFSET, pDataArray(i)
    If heap(i) = 0 Then
        ExitProcess 2
    End If
    pDataArray(i).val1 = i
    pDataArray(i).val2 = i + 100
    hThreadArray(i) = CreateThread(0, 0, MyThreadFunc, _Offset(pDataArray(i)), 0, _Offset(dwThreadIdArray(i)))
    If hThreadArray(i) = 0 Then
        ErrorHandler "CreateThread"
        ExitProcess 3
    End If
Next
WaitForMultipleObjects MAX_THREADS, _Offset(hThreadArray()), 1, INFINITE
For i = 1 To MAX_THREADS
    CloseHandle hThreadArray(i)
    If heap(i) <> 0 Then
        HeapFree GetProcessHeap, 0, heap(i)
    End If
Next
CloseHandle ghMutex
FreeLibrary libload

Function MyThreadFunction& (lpParam As _Offset)
    Dim As String * BUF_SIZE msgBuf
    Dim As _Offset hStdout
    Dim As Long cchStringSize, dwChars
    Dim As MyData MyData
    hStdout = GetStdHandle(STD_OUTPUT_HANDLE)
    If hStdout = INVALID_HANDLE_VALUE Then
        MyThreadFunction = 1
    End If
    Dim As _MEM PMYDATA: PMYDATA = _MemNew(8)
    _MemPut PMYDATA, PMYDATA.OFFSET, lpParam
    _MemGet PMYDATA, PMYDATA.OFFSET, MyData
    StringCchPrintf _Offset(msgBuf), BUF_SIZE, "Parameters = %d, %d" + Chr$(10) + Chr$(0), MyData.val1, MyData.val2
    StringCchLength _Offset(msgBuf), BUF_SIZE, _Offset(cchStringSize)
    WriteConsole hStdout, _Offset(msgBuf), cchStringSize, _Offset(dwChars), 0
    MyThreadFunction = 0
End Function

Sub ErrorHandler (lpszFunction As String)
    Dim As _Offset lpMsgBuf, lpDisplayBuf
    Dim As Long dw: dw = GetLastError
    FormatMessage FORMAT_MESSAGE_ALLOCATE_BUFFER Or FORMAT_MESSAGE_FROM_SYSTEM Or FORMAT_MESSAGE_IGNORE_INSERTS, 0, dw, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), _Offset(lpMsgBuf), 0, 0
    lpDisplayBuf = LocalAlloc(LMEM_ZEROINIT, (lstrlen(lpMsgBuf) + lstrlen(_Offset(lpszFunction)) + 40) * sizeoftchar)
    StringCchPrintf2 lpDisplayBuf, LocalSize(lpDisplayBuf) / sizeoftchar, "%s failed with error %d:" + Chr$(10) + " %s" + Chr$(0), lpszFunction + Chr$(0), dw, lpMsgBuf
    MessageBox 0, lpDisplayBuf, "Error" + Chr$(0), MB_OK
    LocalFree lpMsgBuf
    LocalFree lpDisplayBuf
End Sub

I wonder if this code still works. I haven't tried it in 3 years lol

EDIT: Seems to. Cool
   
Tread on those who tread on you

Reply
#17
Clearly a Windows only approach. I'm curious if it is faster than the POSIX pthreads. Thank you for sharing this.
Reply
#18
I was browsing the old forums, and found this tidbit here from Galleon: https://qb64forum.alephc.xyz/index.php?t...#msg132131

Quote: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: (Select All)

c = addnums(5, 6)
FUNCTION addnums (a, b)
    addnums = a + b
END FUNCTION
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.
Reply
#19
I had read that, since I started that thread. The Mandelbrot demo I have on another thread calls the same function up to 32 different threads. I'm aware that the subroutines will be limited and that fits my use.

Did you try the demos? Did they give you issues?
Reply
#20
(06-02-2024, 01:11 PM)justsomeguy Wrote: I had read that, since I started that thread. The Mandelbrot demo I have on another thread calls the same function up to 32 different threads. I'm aware that the subroutines will be limited and that fits my use.

Did you try the demos? Did they give you issues?

I honestly haven't, as my experience with bothering with threading could probably be shared in a 20-second lecture.  I wouldn't know what the heck I was looking at and seeing, while I was seeing it.  Wink

I just found that old quote to be rather amusing, as -- from the way it sounds with the thread here --  you're working out the glitches and sorting out how to make it do what you want it to do.

Maybe you'll need to try your hand at inventing General AI next.  Big Grin
Reply




Users browsing this thread: 6 Guest(s)