A multi-treading example in ILE-RPG using Pthread APIs

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 %editc() or %char() 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.

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:

2023-01-30-11.25.49.213: Main: start of worker #1
2023-01-30-11.25.49.214: Worker #1: has thread-id 00000000:0000002c.
2023-01-30-11.25.49.215: Worker #1: setcalcelstate RC=0
2023-01-30-11.25.49.215: Worker #1: setcalceltype RC=0
2023-01-30-11.25.49.215: Worker #1: waiting 5 seconds ...
2023-01-30-11.25.49.215: Main: start of worker #2
2023-01-30-11.25.49.215: Main: start of worker #3
2023-01-30-11.25.49.215: Main: start of worker #4
2023-01-30-11.25.49.215: Main: waiting 6 seconds ...
2023-01-30-11.25.49.215: Worker #2: has thread-id 00000000:0000002d.
2023-01-30-11.25.49.215: Worker #2: setcalcelstate RC=0
2023-01-30-11.25.49.215: Worker #2: setcalceltype RC=0
2023-01-30-11.25.49.215: Worker #2: waiting 4 seconds ...
2023-01-30-11.25.49.215: Worker #3: has thread-id 00000000:0000002e.
2023-01-30-11.25.49.215: Worker #3: setcalcelstate RC=0
2023-01-30-11.25.49.215: Worker #3: setcalceltype RC=0
2023-01-30-11.25.49.215: Worker #3: waiting 3 seconds ...
2023-01-30-11.25.49.216: Worker #4: has thread-id 00000000:0000002f.
2023-01-30-11.25.49.216: Worker #4: setcalcelstate RC=0
2023-01-30-11.25.49.216: Worker #4: setcalceltype RC=0
2023-01-30-11.25.49.216: Worker #4: waiting 2 seconds ...
2023-01-30-11.25.51.246: Worker #4: waiting 2 seconds ...
2023-01-30-11.25.52.241: Worker #3: waiting 3 seconds ...
2023-01-30-11.25.53.245: Worker #2: waiting 4 seconds ...           
2023-01-30-11.25.53.276: Worker #4: waiting 2 seconds ...
2023-01-30-11.25.54.245: Worker #1: waiting 5 seconds ...
2023-01-30-11.25.55.245: Worker #3: waiting 3 seconds ...
2023-01-30-11.25.55.246: Main: cancel worker #1 RC=0
2023-01-30-11.25.55.246: Worker #1: ending abnormally after 6.03240s
2023-01-30-11.25.55.306: Worker #4: waiting 2 seconds ...
2023-01-30-11.25.57.271: Worker #2: waiting 4 seconds ...
2023-01-30-11.25.57.271: Main: waiting for all workers ...
2023-01-30-11.25.57.336: Worker #4: waiting 2 seconds ...
2023-01-30-11.25.58.275: Worker #3: waiting 3 seconds ...
2023-01-30-11.25.59.366: Worker #4: ending normally after 10.15036s.
2023-01-30-11.25.59.367: Main: joining worker #4 RC=0
2023-01-30-11.26.01.301: Worker #2: waiting 4 seconds ...
2023-01-30-11.26.01.301: Worker #3: waiting 3 seconds ...
2023-01-30-11.26.04.331: Worker #3: ending normally after 15.11601s.
2023-01-30-11.26.04.332: Main: joining worker #3 RC=0
2023-01-30-11.26.05.328: Worker #2: waiting 4 seconds ...
2023-01-30-11.26.09.358: Worker #2: ending normally after 20.14315s.
2023-01-30-11.26.09.359: Main: joining worker #2 RC=0
2023-01-30-11.26.09.359: Main: joining worker #1 RC=0
2023-01-30-11.26.09.359: 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.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert