Posts: 1,000
Threads: 50
Joined: May 2022
Reputation:
27
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);
}
Posts: 303
Threads: 10
Joined: Apr 2022
Reputation:
44
(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.
Posts: 128
Threads: 17
Joined: Apr 2022
Reputation:
10
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.
Posts: 303
Threads: 10
Joined: Apr 2022
Reputation:
44
(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.
Posts: 128
Threads: 17
Joined: Apr 2022
Reputation:
10
@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.
Posts: 730
Threads: 30
Joined: Apr 2022
Reputation:
43
05-30-2024, 06:54 PM
(This post was last modified: 05-30-2024, 07:07 PM by SpriggsySpriggs.)
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
Posts: 128
Threads: 17
Joined: Apr 2022
Reputation:
10
Clearly a Windows only approach. I'm curious if it is faster than the POSIX pthreads. Thank you for sharing this.
Posts: 2,697
Threads: 328
Joined: Apr 2022
Reputation:
217
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.
Posts: 128
Threads: 17
Joined: Apr 2022
Reputation:
10
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?
Posts: 2,697
Threads: 328
Joined: Apr 2022
Reputation:
217
(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.
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.
|