Wednesday, March 27, 2019

Delaying a job for a fraction of a second

pause a program for a fraction of a second

Someone asked me if there was a way to pause a job for a fraction of a second. Their colleague had advised them to create a Do-loop that would be performed a certain number of times and that would be "good enough". I was very pleased to learn that the person who asked this question was not satisfied with that answer, and correctly thought that there must be a better way.

I am sure we are all know of the Delay Job command, DLYJOB, but it will only delay a job for whole seconds and not fractions. Fortunately IBM i has two external C functions that can be used to delay a job:

Function Description Maximum value
sleep Will delay a job for whole seconds 2,147,483,647 seconds
Or 18 days 3 hours 14 minutes 7 seconds
usleep Will delay a job for a fraction of one second 1,000,000 microseconds
Or 1 second

They are extremely easy to use, and I will be giving examples of how to use both. I have written about the sleep function before, but not in the level detail I have here.

I think it is easiest to show how to use these functions by giving a very simple example:

01  **free
02  ctl-opt dftactgrp(*no) ;

03  dcl-pr sleep int(10) extproc('sleep') ;
04    *n uns(10) value ;
05  end-pr ;

06  dcl-pr usleep int(10) extproc('usleep') ;
07    *n uns(10) value ;
08  end-pr ;

09  dcl-s Returned int(10) ;

10  Returned = sleep(1) ;
11  if (Returned < 0) ;
12    dsply ('Error!') ;
13  endif ;

14  Returned = usleep(800000) ;
15  if (Returned < 0) ;
16    dsply ('Error!') ;
17  endif ;

Line 1: Totally free RPG rocks!

Line 2: As I will be calling external procedures I need the DFTACTGRP(*NO) as I cannot run this program in the default activation group.

Lines 3 – 5: This is the procedure prototype for sleep. C functions are defined as external procedures, EXTPROC, in RPG. I have to give the name of the function in the EXPROC keyword, I also have to be careful as the name is case sensitive. The INT(10) on the same line means that this procedure will return a 10 long integer value, this is "0" if the procedure completed normally and less than zero if it did not. Line 4 shows the only parameter that is passed to this procedure. I never name the parameters in the prototype, therefore, I have to give *N in place of a name. This variable is a 10 long unsigned integer variable.

Lines 6 – 8: The definition of the prototype for usleep is identical.

Line 9: This variable will be used to contain the returned value from the procedures.

Line 10: The sleep procedure is called passing a value of 1 second. The value returned from the procedure is placed in the Returned variable.

Lines 11 – 13: If the value in Returned is less than zero then an error occurred in sleep, and this program will display "Error!".

Lines 14: usleep procedure is called with a value of 800,000 milliseconds (0.8 seconds). If there is an error in the procedure, like with sleep, a negative value returned into Returned.

Lines 15 – 17: If an error occurred in usleep then "Error!" will be displayed.

In "real life" I have placed the prototypes for these C functions into my prototypes source member, that way I can just copy the prototype into any program or procedure I need.

01  /if defined(sleep)
02  dcl-pr sleep int(10) extproc('sleep') ;
03    *n uns(10) value ;
04  end-pr ;
05  /endif

06  /if defined(usleep)
07  dcl-pr usleep int(10) extproc('usleep') ;
08    *n uns(10) value ;
09  end-pr ;
10  /endif

Lines 1 and 6: I always use the /IF DEFINED as when I want to copy snippets from the prototypes source member I only get the bits I want and not the whole source member.

01  /define sleep
02  /define usleep
03  /include mylib/devsrc,prototypes
04  /undefine sleep
05  /undefine usleep

Lines 1 and 2: The DEFINE tells the compiler which parts of the prototype source member I want included in this program or procedure.

Lines 4 and 5: While not mandatory, I like to undefine what I have defined.

If I am going to delay a program by milliseconds how exact is that delay?

To find out I created a program that would call sleep, the CL command DLYJOB called by the QCMDEXC API, and usleep. I am going to call them 300 times, and then calculate the average time it took for the function, or program in QCMDEXC's case, to complete.

To make it easier to explain what is happening I am going to separate this program into two parts. Let me start with the definitions:

01  **free
02  ctl-opt main(Main) option(*srcstmt) dftactgrp(*no) ;

03  dcl-f OUTFILE usage(*output) usropn ;

04  dcl-pr sleep extproc('sleep') ;
05    *n uns(10) value ;
06  end-pr ;

07  dcl-pr usleep extproc('usleep') ;
08    *n uns(10) value ;
09  end-pr ;

10  dcl-pr QCMDEXC extpgm ;
11    *n char(20) options(*varsize) const ;
12    *n packed(15:5) const ;
13  end-pr ;

14  dcl-s Count uns(5) ;
15  dcl-s Start timestamp ;
16  dcl-s End like(Start) ;

Line 2: I have added a couple of other keywords into the control options. The first states that this program will have a Main procedure, rather than use the RPG cycle. The second is the *SRCSTMT option, this means that the source statement number will be used in the program when it is created. This makes it a lot easier when errors happen I can match the line number reported to the source line number.

Line 3: This is the file that will contain the information from my tests. I am not going to show its layout as it is not necessary for this example.

Lines 4 – 9: I have defined both the sleep and usleep prototypes in the source member. Notice that I do not want any values back from the functions, as the INT(10) is absent.

Lines 10 – 13: This is the prototype for the QCMDEXC API.

Lines 14 – 16: Definition of variables that will be used in the program. To my surprise I was able to define a variable called End, line 16, as I thought was a RPG reserved word.

And now onto the Main procedure:

17  dcl-proc Main ;
18    open OUTFILE ;

19    for Count = 1 to 300 ;
20      TYPE = 'SLEEP' ;
21      Start = %timestamp() ;
22      sleep(1) ;
23      End = %timestamp() ;
24      DIFF = %diff(End:Start:*ms) ;
25      write OUTFILER ;

26      TYPE = 'DLYJOB' ;
27      Start = %timestamp() ;
28      QCMDEXC('DLYJOB DLY(1)':13) ;
29      End = %timestamp() ;
30      DIFF = %diff(End:Start:*ms) ;
31      write OUTFILER ;

32      TYPE = 'USLEEP' ;
33      Start = %timestamp() ;
34      usleep(500000) ;
35      End = %timestamp() ;
36      DIFF = %diff(End:Start:*ms) ;
37      write OUTFILER ;
38    endfor ;

39  on-exit ;
40    close *all ;
41  end-proc ;

Line 17: Start of the Main procedure. There is no need for a procedure interface as this procedure is not passed any parameters.

Line 18: Open the output file.

Line 19: I am going to execute this For-loop 300 times.

Lines 20 – 25: This block of code is used to record the how long the sleep function takes for 1 second. By capturing the timestamp before sleep, line 21, and after, line 23, it is possible to calculate the difference, line 24, which is written to the output file, line 25.

Lines 26 – 31: The next block of code does the same thing for the DLYJOB command which is executed by the QCMDEXC API.

Lines 32 – 37: Finally usleep is called with a delay of half a second.

Lines 39 and 40: At the end of the program I have inserted an ON-EXIT section to ensure that the open file is closed no matter whether the program ends normally or abnormally.

Now I can use the following SQL statement to get my results:

SELECT TYPE,COUNT(*),AVG(DIFF)
  FROM OUTFILE
 GROUP BY TYPE

I am selecting the field TYPE, count of the number of records, and the average value of the field DIFF from those records. By using the group by the TYPE field I get the average and count per type.

TYPE    COUNT ( * )    AVG ( DIFF )
------  -----------   -------------
DLYJOB          300   1,027,963.333
SLEEP           300   1,027,470.000
USLEEP          300     527,935.000

Note: The DIFF column is in microseconds.

When using the DLYJOB or the sleep 2 hundredth of a second may not be significant. But for the usleep, that delays for fractions of a second, it is.

What causes this delay? IBM's documentation gives this as the reason: "Because of processor delays, the thread can be suspended slightly longer than this specified time."

So be warned if you are going to use usleep your request of a delay for half a second may be longer.

 

You can learn more about this from the IBM website:

 

This article was written for IBM i 7.3, and should work for some earlier releases too.

5 comments:

  1. "I never name the parameters in the prototype,..."

    Why not? Even though the name is optional, I do anyway. This may make the code more readable to the next developer. Thanks.

    iMilliSecs uns(10) value ;

    Ringer

    ReplyDelete
    Replies
    1. I completely agree with Ringer. I almost? always name the parameters in a prototype. Without naming the parameters for sleep and usleep, it's impossible to guess what the difference is.

      Delete
  2. (I meant iMicroSecs)

    Ringer

    ReplyDelete
  3. Simon, thanks for sharing. I have used the sleep function many times , but the unsleep function is new to me.. frictional sleeping.. totally outstanding.. again, thanks for sharing another teaching moment for you. Have a great weekend..

    ReplyDelete
  4. Instead of repeating the procedure name in EXTPROC, how about using EXTPROC(*DCLCASE) instead. For simple names like 'sleep', it doesn't really matter, but for more complex names like 'Qp0lCvtPathToQSYSObjName', I think it's nicer to only specify the name once in the prototype.

    ReplyDelete

To prevent "comment spam" all comments are moderated.
Learn about this website's comments policy here.

Some people have reported that they cannot post a comment using certain computers and browsers. If this is you feel free to use the Contact Form to send me the comment and I will post it for you, please include the title of the post so I know which one to post the comment to.