RPG

 View Only

Multithreading ILE-RPG with pthreads

By Daniel Gross posted Fri November 15, 2024 02:20 PM

  

If your application needs multithreading, the POSIX API for multithreading – called pthreads – is the classic way, to run multiple procedures parallel at the same time and in the same process. Using it in ILE-RPG is quite easy, if you know how.

This example also shows, how to use standard C library functions like printf() and sprintf() in your RPG programs. Especially sprintf() can be a real time saver to format numbers to your needs, when %char() oder %editc() aren’t enough.

Lets break down the program in pieces – we start at the top:

We use free-format RPG – of course – and some standard headers. In this example, I’m using a „linear“ main procedure – no RPG cycle, no *INLR – and a named activation group. The APIs should work the same with a cycle main procedure or an activation group *NEW or even with QILE – but I always recommend not to use the QILE activation group.
The control keyword THREAD(*CONCURRENT), tells the compiler, that it should create a separate static storage for each thread in the program. This means, that module global variables are NOT shared between the threads. If you don't code this keyword, ALL global variables are shared between all threads - and even if you don't use any global variables, RPG always has some internal global fields. You can use the keyword STATIC(*ALLTHREAD) on a static variable to define it as static storage that is shared between all threads. (Thanks to Barbara Morris to give me that hint!)
Alternatively you can use the keyword THREAD(*SERIALIZE) to tell the compiler that the whole module is serialized - but with that, you can't run your threads concurrently - so this wouldn't work here.
We don’t need special service programs or binder directories to use the pthreads APIs – but we include the system headers „pthread“ (maybe obvious) and „unistd“ (for the sleep() procedure) from QSYSINC/QRPGLESRC
We want to give our „worker“ threads some parameters – and as you can only pass one (!) parameter (a pointer) via the API to your „workers“, we create a template data structure for the „caller“ and the „workers“.
Now to the Main procedure:

Our main procedure is called Main (very creative, isn’t it?) and has no parameters.

To remember our threads, we use a data structure array (lines 001900 to 002200), which consists of two sub data structures – one of type pthread_t, which is defined in the pthread include and which will hold the information about each thread.

The other sub data structure uses the worker parameter template, that we defined above. We will store the parameters for each thread there. You will ask yourself, why do we use a seperate parameter structure for each thread?

The answer is simple – as we pass a pointer to that parameter structure, we shouldn’t modify it after the thread is „launched“. As we want each thread to have its own parameters – isolated from the other threads – we use a separate data structure for each thread.

We also declare some other variables (lines 002400 to 002600) – a loop counter, the return code of the Pthread APIs and a flag for the on-exit section, to detect whether the procedure was ended normally (by return) or abnormal (by an exception).

The in next part (lines 002800 to 004100) we use a simple loop to create 4 „worker threads. We initialize the worker parameters and call the most important API pthread_create(). After that we wait for 6 seconds using the sleep() function.

The print() procedure is just a simple wrapper for the C printf() function. As out program can only run in batch, the output will land in a spooled file QPRINT. We will habe a look at the output later. The source code for the print() procedure can be found in the complete source below.

Now something funny – we will cancel the 1st worker thread (line 004400) by calling pthread_cancel() passing the matching pthread_t data structure. Worker #1 is the longest running thread – so after 6 seconds it is definitely still running.

The last part (lines 004700 to 005000) is „joining“ the worker thread to the main procedure. This is done by calling pthread_join(). Each call waits for the passed thread to finish, detaches it and returns the thread exit status.

Our Worker procedure is also quite simple:

We use a small procedure getThreadId() which simply retrieves the id of thread and formats it as a string using sprintf(). The source code for the getThreadId() procedure can be found in the complete source below.

The worker sets its own thread attributes cancelstate and canceltype. We want our threads to be cancelable and do asynchronous cancel. This means, that when the thread gets canceled from „outside", it will end immediately.

The „work“ that is done is just for the demonstration and to give the main procedure some time.

There is also the on-exit block in the Worker procedure. This will not only „catch“ runtime exceptions, but also if the thread gets canceled – you will see this here in the example output:

Main: start of worker #1
Worker #1: has thread-id 00000000:0000002c.
Worker #1: setcalcelstate RC=0
Worker #1: setcalceltype RC=0
Worker #1: waiting 5 seconds ...
Main: start of worker #2
Main: start of worker #3
Main: start of worker #4
Main: waiting 6 seconds ...
Worker #2: has thread-id 00000000:0000002d.
Worker #2: setcalcelstate RC=0
Worker #2: setcalceltype RC=0
Worker #2: waiting 4 seconds ...
Worker #3: has thread-id 00000000:0000002e.
Worker #3: setcalcelstate RC=0
Worker #3: setcalceltype RC=0
Worker #3: waiting 3 seconds ...
Worker #4: has thread-id 00000000:0000002f.
Worker #4: setcalcelstate RC=0
Worker #4: setcalceltype RC=0
Worker #4: waiting 2 seconds ...
Worker #4: waiting 2 seconds ...
Worker #3: waiting 3 seconds ...
Worker #2: waiting 4 seconds ...
Worker #4: waiting 2 seconds ...
Worker #1: waiting 5 seconds ...
Worker #3: waiting 3 seconds ...
Main: cancel worker #1 RC=0
Worker #1: ending abnormally after 6.03240s
Worker #4: waiting 2 seconds ...
Worker #2: waiting 4 seconds ...
Main: waiting for all workers ...
Worker #4: waiting 2 seconds ...
Worker #3: waiting 3 seconds ...
Worker #4: ending normally after 10.15036s.
Main: joining worker #4 RC=0
Worker #2: waiting 4 seconds ...
Worker #3: waiting 3 seconds ...
Worker #3: ending normally after 15.11601s.
Main: joining worker #3 RC=0
Worker #2: waiting 4 seconds ...
Worker #2: ending normally after 20.14315s.
Main: joining worker #2 RC=0
Main: joining worker #1 RC=0
Main: ending normally.

As you can see, the messages of the Main procedure and the workers are interleaved – so the workers are really running in parallel to each other and to the Main procedure.

The cancelation of worker #1 is taking place immediately – thanks to our thread attributes – and the cancel leads to an abnormal end of the thread – which is nice to know, because you are able, to catch that.

It is also good to know, that pthread_join() is waiting for the given thread to end when it is called, and that you won’t receive an error, when the thread you are trying to join was already canceled previously.

Last but not least – multi-threading is only allowed for batch jobs which are submitted with the ALWMLTTHD(*YES) parameter. Interactive jobs can’t use pthreads at all.

You can find the complete program source code in my examples repository on GitHub. This includes the procedures not shown here and is ready to be cloned or copied and compiled.

I hope you enjoyed this small example.

2 comments
17 views

Permalink

Comments

Fri November 22, 2024 09:32 AM

Hi Barbara,

thanks for that hint - I think I will update the blog with that.

- Daniel

Fri November 22, 2024 08:31 AM

Hi Daniel,

You should have the THREAD(*CONCURRENT) keyword in the CTL-OPT statements. Without that keyword, the static storage in your module could be updated by multiple threads at the same time, possibly causing insane behaviour in the module. Even if you don't have any static variables coded, there's lots of static storage that RPG uses to control the module.

In general, for any RPG module that might run in multiple threads, you should always add the THREAD keyword in the CTL-OPT statements, either THREAD(*SERIALIZE) or THREAD(*CONCURRENT).

But in this case, you want *CONCURRENT for sure since you intend for the thread procedures to run concurrently :-)

If you have any static variables that you want all the threads to share, define them with STATIC(*ALLTHREAD).

- Barbara