[prev] [up] [next]

Working with Time, Delays and Interrupts

Contents

Introduction

This document will teach you how to use the timing facilities of Smalltalk/X. Typically, time handling is done by delaying a process (a smalltalk-lightweight process) for some time delta. There are also other (more tricky) possibilities.

Please read the document ``Working with processes'' if you do not yet know about processes, priorities, suspension etc.

The current Time vs. the millisecondClock

The internals of smalltalk use different facilities for time representation. Instances of Time, which represent time-of-day values, of AbsoluteTime, which represent a particular time at a specific date and finally millisecondClock values, which are the values of a free running clock, incremented every millisecond.

Currently, all delaying is internally based upon the millisecondClock value. This value has only a limited precision (i.e. number of bits) and therefore overruns in regular intervals. Although the details depend on the particular OperatingSystem internals, this interval guaranteed to be greater than 24 hours.
On Unix, a flip through 0 occurs about every 74 hours. Since Smalltalk/X's smallIntegers are represented in 31 bits, it is (currently) not possible to represent time intervals longer than HALF of the range (i.e. about 37 hours) using the millisecondClock value.
(see OperatingSystem » maximumMillisecondTimeDelta)

Therefore, it is (currently) not possible to directly use delays on times longer than this interval. To do this, you have to write a little program doing this with a workaround (i.e. delaying in smaller steps until the desired time and/or time-of-day has passed).

The operatingSystems millisecondClock offers a limited precision. Typical internal resolutions are 1/100, 1/60, 1/50 or even 1/20 of a second. Thus, when asking for the millisecondClocks value after a short time interval may return you the same value as before, even when some milliseconds actually have passed in between.

Since millisecondClock values wrap, you cannot perform arithmetic directly upon them. Class OperatingSystem provides methods which know about the wrap and handle it correctly.
See:

    OperatingSystem » millisecondTimeDeltaBetween:and:
    OperatingSystem » millisecondTimeAdd:and:
    OperatingSystem » millisecondTime:isAfter:
Note:
We are not perfectly happy with this solution - there ought to be some "WrappingNumber" subclass of Number, which does all this transparently.

Using Delay

Delay can be used to suspend the currently running process for some time.
A delay object for some time interval is created with:
    d := Delay forSeconds:numberOfSeconds
or:
    d := Delay forMilliseconds:numberOfMillis
The actual delaying is done by sending wait to this delay.
Try evaluating:
    |d|

    d := Delay forSeconds:3.
    d wait
you will notice, that only the current process is suspended for the delay time. Other processes continue to execute (open some animated demo - for example, the CubeDemo - to see this).

For short delays, use forMilliseconds, as in:

    (Delay forMilliseconds:100) wait
There are also `combined create & wait' interfaces available in the Delay class:
    Delay waitForMilliseconds:100
or:
    Delay waitForSeconds:10

Delay-time errors and how to avoid them

The delay will last for at least the specified time, it usually waits a bit longer due to internal overhead (the scheduler switching processes) or if a higher priority process becomes runnable in the meantime. Also, no operating system can guarantee exact times; even realtime systems (like the realIX/88k) only guarantee an upper limit for delays.
For example, another unix process may be executing at a higher priority and thus lead to a slightly longer delay.

This means, that if you call for a delay in a loop, the individual errors will accumulate. It is therefore not possible, to maintain a heart-beat kind of time handling by simply performing delays in a loop.
The example:

    |d t1 t2 delta|

    t1 := Time millisecondClockValue.
    d := Delay forMilliseconds:50.
    100 timesRepeat:[
	d wait.
	"do something"
    ].
    t2 := Time millisecondClockValue.
    delta := OperatingSystem millisecondTimeDeltaBetween:t2 and:t1.
    Transcript show:'delta time is '; show:(delta); showCR:' milliseconds'.
will NOT finish after 5 seconds, but some time later. On an unloaded system, the error is typically a few percent, but, depending on other processes running on your machine, the error may become substantial.

Also, the actual delay time also depends on the resolution of the machines clock: for example, if the minimum resolution is 60ms, all delays smaller than that time interval will always delay for at least that time.
(therefore, the above example may take 6 or even 10 seconds to complete, with clock resolutions of 60ms or 100ms !)

To fix this problem, Delay allows waiting for a specific time to be reached, (in addition to the deltatime based wait described above).
Try:

    |t1 t2 now then delta|

    t1 := Time millisecondClockValue.
    now := Time millisecondClockValue.
    100 timesRepeat:[
	then := OperatingSystem millisecondTimeAdd:now and:50.
	(Delay untilMilliseconds:then) wait.
	now := then
    ].
    t2 := Time millisecondClockValue.
    delta := OperatingSystem millisecondTimeDeltaBetween:t2 and:t1.
    Transcript show:'delta time is '; show:(delta); showCR:' milliseconds'.
of course, there is also a nonzero error in each individual wait, but this error will not accumulate.
Therefore, the relative error will approach zero over time. Use the above algorithm, if you need some action to be performed in constant intervals.

Premature wakeup

A process waiting on a delay can be resumed before its actual delay-time has expired. To do so, send #resume to the delay.
Of course, this must be done by some other process (the delayed process itself obviously cant do it).
Example:
    |process d|

    process :=
	[
	    Transcript show:(Time now);
		       showCR:' subprocess: going to wait for half an hour ...'.
	    d := Delay forSeconds:1800.
	    d wait.
	    Transcript show:(Time now);
		       showCR:' subprocess: here I am again ...'.
	    Transcript show:(Time now);
		       showCR:' subprocess: done.'
	] fork.

    "after some short time, stop the wait"

    Transcript show:(Time now); showCR:' main process: wait a bit'.
    Delay waitForSeconds:2.
    Transcript show:(Time now); showCR:' main process: wakeup the delay early now'.
    d resume.
    Transcript show:(Time now); showCR:' main process: done.'.
Process priorities also have an influence; in the following example, the subprocess will start right-away and resume immediately (before the parent process outputs the 'done' message):
    |process d|

    process :=
	[
	    Transcript show:(Time now);
		       showCR:' subprocess: going to wait for half an hour ...'.
	    d := Delay forSeconds:1800.
	    d wait.
	    Transcript show:(Time now);
		       showCR:' subprocess: here I am again ...'.
	    Transcript show:(Time now);
		       showCR:' subprocess: done.'
	] forkAt:(Processor activePriority + 1).

    "after some short time, stop the wait"

    Transcript show:(Time now); showCR:' main process: wait a bit'.
    Delay waitForSeconds:2.
    Transcript show:(Time now); showCR:' main process: wakeup the delay early now'.
    d resume.
    Transcript show:(Time now); showCR:' main process: done.'.

Installing a timed wakeup on any other semaphore

If you look at the implementation of delays and semaphores, you will notice that the actual work is done in the ProcessorScheduler class. Through the global variable Processor (which is the one-and-only instance of ProcessorScheduler) you can tell the processor to signal a semaphore whenever some time has been reached, or input arrives on an external stream.
This allows installation of a timeout, while waiting for some input to arrive.

The following example waits until either some input arrives on a pipe-stream, or 5 seconds have expired,
Actually, there is already a method which does exactly this:

    ExternalStream readWaitWithTimeout:
we show the code here anyway, for didactic reasons:
    |sema pipe|

    "/ create a pipeStream - there will be data after 1 second:

    pipe := PipeStream readingFrom:'(sleep 1; echo hello)'.
    Transcript show:Time now; showCR:' pipe created'; endEntry.

    "/ create a semaphore, arrange for it to be signalled when
    "/ either data arrives, or 5 seconds have passed

    sema := Semaphore new.
    Processor signal:sema onInput:(pipe fileDescriptor).
    Processor signal:sema afterMilliseconds:5000.

    "/ now wait

    sema wait.

    Transcript show:Time now; showCR:' after wait'; endEntry.

    "/ data or timeout ?

    pipe canReadWithoutBlocking ifTrue:[
	Transcript show:Time now; showCR:' data available'; endEntry
    ] ifFalse:[
	Transcript show:Time now; showCR:' no data available'; endEntry
    ].

    "/ cleanup

    Transcript show:Time now; showCR:' closing pipe'; endEntry.
    pipe shutDown.
    Transcript show:Time now; showCR:' done'; endEntry.
in the previous example, the semaphore was signalled by data arriving in the pipe; in the following, a timeOut will trigger the semaphore:
    |sema pipe|

    "/ create a pipeStream - there will be data after 15 second:

    pipe := PipeStream readingFrom:'(sleep 15; echo hello)'.
    Transcript show:Time now; showCR:' pipe created'; endEntry.

    "/ create a semaphore, arrange for it to be signalled when
    "/ either data arrives, or 5 seconds have passed

    sema := Semaphore new.
    Processor signal:sema onInput:(pipe fileDescriptor).
    Processor signal:sema afterMilliseconds:5000.

    "/ now wait

    sema wait.
    Transcript show:Time now; showCR:' after wait'; endEntry.

    "/ data or timeout ?

    pipe canReadWithoutBlocking ifTrue:[
	Transcript show:Time now; showCR:' data available'; endEntry
    ] ifFalse:[
	Transcript show:Time now; showCR:' no data available'; endEntry
    ].

    "/ cleanup

    Transcript show:Time now; showCR:' closing pipe'; endEntry.
    pipe shutDown.
    Transcript show:Time now; showCR:' done'; endEntry.
Notice:
The close method in pipeStream waits for the underlying unix command to finish correctly (this is done in the underlying pclose() system function and not a smalltalk feature).
Therefore, we better use the alternative shutDown method (which does not wait) to close the pipeStream in the second example.
You can use shutDown just like close with ordinary streams - they behave the same, except for pipeStreams and socket connections.

using the existing waitWithTimeout-mechanism from ExternalStream the above is of course the same as:

    |pipe|

    "/ create the pipe - data will arrive after 15 seconds

    pipe := PipeStream readingFrom:'(sleep 15; echo hello)'.
    Transcript show:Time now; showCR:' pipe created'; endEntry.

    "/ wait, but no longer than 5 seconds

    (pipe readWaitWithTimeout:5) ifTrue:[
	Transcript show:Time now; showCR:' data available'; endEntry
    ] ifFalse:[
	Transcript show:Time now; showCR:' no data available'; endEntry
    ].

    "/ cleanup

    Transcript show:Time now; showCR:' closing pipe'; endEntry.
    pipe shutDown.
    Transcript show:Time now; showCR:' done'; endEntry.
Try it with data arriving earlier in a workspace.

Timed interrupts

Using delays, timing is done by suspending a process for some interval. It is also possible, to continue execution and arrange for an interrupt to occur after some time and let the process continue to run.
To do this, you have to define a block (which will be evaluated by the process after the time has passed) and install it with:
    Processor addTimedBlock:aBlock afterSeconds:seconds
or:
    Processor addTimedBlock:aBlock afterMilliseconds:millis
or:
    Processor addTimedBlock:aBlock atMilliseconds:aMillisecondsClockValue
The currently running process will be interrupted in whatever it is doing when the time has come; if suspended, it will be resumed.
Since the behavior is as if the process did "aBlock value", you can perform all kind of actions in the block: raise a signal, do a block-return, terminate the process etc.

You can also force an immediate interrupt and have another process evaluate a block:

    aProcess interruptWith:aBlock
to arrange for this to occur after some time, use:
    Processor addTimedBlock:[
	aProcess interruptWith:aBlock
    ] afterSeconds:timeTillInterrupt
example:
    Processor
	addTimedBlock:[Transcript showCR:'interrupt occured'; endEntry]
	afterSeconds:1.

    Transcript showCR:'start processing ...'; endEntry.
    1 to:10 do:[:i |
	Transcript showCR:i; endEntry.
	1000 factorial
    ].
    Transcript showCR:'done.'; endEntry
example:
    |p|

    p := [
	    Transcript showCR:'subprocess start.'; endEntry.
	    1 to:20 do:[:i |
		Transcript showCR:i; endEntry.
		1000 factorial.
	    ].
	    Transcript showCR:'subprocess end.'; endEntry.
    ] forkAt:4.

    Transcript showCR:'waiting for a while ...'; endEntry.
    (Delay forSeconds:3) wait.

    Transcript showCR:'now killing subprocess ...'; endEntry.
    p interruptWith:[p terminate].

    Transcript showCR:'done.'; endEntry
Notice: The system uses this interrupt mechanism to interrupt a process when "Ctrl-C" is pressed.

Lowlevel interrupts

All of the above examples involved a process to react somehow on an incoming interrupt (either directly by interrupting the process to perform some action, or indirectly by signalling a semaphore).

On the lowest level, the runtime system reacts to interrupts by sending messages to so called interrupt handler objects at the time the interrupt occurs.
These objects are responsible for signalling semaphores, rescheduling processes and to implement the above described highlevel interrupt behavior.
At system startup time, Smalltalk/X installs appropriate handler objects as interrupt handler.
(see "Smalltalk initInterrupts" , or "ProcessorScheduler initialize" for some examples which install low level handlers.)

You can (*) install your own interrupt handler objects; situations in which this makes sense are:

You get a faster interrupt response, because the handler object gets called right in the context of whichever process is currently running (i.e. there is no automatic switch to the scheduler or any other).
In normal situations, the general mechanism are fast enough and there is no need to change things - however, if you have to handle interrupts at a very high frequency (say 1000 per second ;-), the context switch times may easily become a limiting factor. Consider this a hook for very specialized real time applications. [there are other things to take into consideration when doing realtime, though.]

Interrupt handler objects and messages

All interrupt handler objects are accessed by the runtime system via class variables in the ObjectMemory class. These class variables are known to the runtime system - you may not remove them (actually, you could do this - the VM would then no longer send any interrupt messages ... but why would you want to do this ?).

The handler variables and messages sent to them are:

Changing interrupt handler objects

The above listed messages are implemented in the Object class. Therefore, every object understands and responds to those messages.

For each handler, ObjectMemory provides a message to get and set the corresponding handler objects. Like signal access methods, these are named after the corresponding handlers variable name.
For example,

    ObjectMemory userInterruptHandler
returns, and
    ObjectMemory userInterruptHandler:someObject
sets the UserInterruptHandler.

(*) WARNING:

You may easily make the system inoperatable when playing around with these handlers. So be prepared for malfunctions or deadlocks when changing things in this area (think twice and save your work before doing so).

(**) Switched with processes - each process may have its own private handler object.


Copyright © 1995 Claus Gittinger, all rights reserved

<cg at exept.de>

Doc $Revision: 1.22 $ $Date: 2021/03/13 18:24:52 $