If the system is running on UNIX and you are already familiar with UNIX processes, you should keep in mind that:
Lightweight processes should not be confused with Unix's processes. UNIX processes have separate address-spaces, meaning that once created, such a process can no longer access and/or modify objects from the parent process.On some systems, which do support native threads, smalltalk processes are mapped to native threads. However, ST/X enforces and makes certain, that only one of them executes at any time.
With UNIX processes, communication must be done explicit via interprocess mechanisms, such as shared memory, sockets, pipes or files.
Also, once forked, files or resources opened by a UNIX subprocess are not automatically visible and/or accessable from the parent process. (This is also true for other child processes).In contrast, smalltalk processes all run in one address space, therefore communication is possible via objects. They share open files, windows and other operating system resources.
#newProcess to
a block. This creates and returns a new process object, which is NOT scheduled
for execution (i.e. it does not start execution).
#resume message.
The separation of creation from starting was done to allow for the new process to be manipulated (if required) before the process has a chance to run. For example, its stackSize limit, priority or name can be changed.
If that is not required, (which is the case with most processes), a combined
message named #fork can be used, which creates and schedules the
process for execution.
By default, the new process gets the current processes priority as
its process priority.
To fork a process with a different priority,
use the #forkAt: message,
which expects the new processes priority as argument.
Read the section below on priorities and scheduling behavior.
examples:
(You may want to start a ProcessMonitor to
see the processes in the system, before executing the examples below.)
forking a process:
|newProcess|
newProcess := [
Delay waitForSeconds:10.
Transcript showCR:'hello there.'
] fork.
forking a process at low priority:
|newProcess|
newProcess := [
Delay waitForSeconds:10.
Transcript showCR:'hello there.'
] forkAt:1.
creating, setting its name and resuming a process:
The
|newProcess|
newProcess := [
Delay waitForSeconds:10.
Transcript showCR:'hello there.'
] newProcess.
newProcess name:'my process'.
newProcess resume
#name: message in the last example sets a processes
name - this has no semantic meaning,
but helps in finding your processes in the ProcessMonitor.
OrderedCollection hold some internal indices to keep track of
first and last elements inside its container array.
If a process
switch occurs during update of these values, the stored value could
be invalid.
SharedQueue - provides a safe implementation of a queue
Semaphore - for synchronization and mutual exclusion
Delay - for timing
RecursionLock - mutual exclusion between processes
Monitor - for non-block critical regions
setup:
writer:
|sema sharedCollection|
...
sema := Semaphore forMutualExclusion.
sharedCollection := OrderedCollection new.
...
reader:
...
sema critical:[
sharedCollection addLast:something
].
...
The
...
sema critical:[
something := sharedCollection removeFirst
].
...
"Semaphore forMutualExclusion" expression creates a
semaphore, which provides safe execution of critical regions.
Critical regions are executed by the #critical: message,
which executes the argument block while asserting, that only one process
is ever within a region controlled by the semaphore.
If any process tries to enter a critical region which has already been
entered, it is suspended until the region is left by the other process.
Sometimes, some process may want to enter another critical region, while
already being within a critical region.
If the new region is controlled by the same sempahore as the previously entered
one, this results in a deadlock situation.
To prevent this, a special semaphore called RecursionLock should
be used; this behaves like a regular semaphore except that it allows
the owning process to reenter a critical region.
If a process terminates, while being within a critical region, that semaphore is automatically released - there is no need for the programmer to care for this.
The above #critical: messages expects a block to be passed;
if your critical region cannot be placed into a block
but is to be left in a different method/class from where it was entered,
you can alternatively use a monitor object.
Monitors provides separate #enter and #exit
methods - so the critical region can be left anywhere else from where it was
entered. However, be careful to always correctly leave those monitors, since
they do not provide any automatic cleanup in case of process termination.
(for example, use a monitor if the lock/unlock
shall be done by the keyPress/keyRelease event handlers),
Using a monitor, the above example is written as:
|monitor|
...
monitor := Monitor new.
...
monitor enter.
... critical code, protected by monitor ...
monitor exit.
...
Simple reader/writer applications are best implemented using SharedQueues,
(which are basically implemented much like above code).
Thus, the above becomes:
writer:
|queue|
...
queue := SharedQueue new.
...
reader:
...
queue nextPut:something.
...
The smalltalk classes could be rewritten to add interlocks at every possible
access in those containers (actually, many other classes such as the complete
...
something := queue next
...
View hierarchy must be rewritten too).
This has not been done, mainly for performance reasons. The typical case
is that most objects are not used concurrently - thus the overhead involved
by locking would hurt the normal case and only simplify some special cases.
examples:
two processes, NOT synchronized (results in corrupted output):
|p1 p2|
p1 := [
10 timesRepeat:[
Transcript show:'here'.
Delay waitForSeconds:0.1.
Transcript show:' is'.
Delay waitForSeconds:0.1.
Transcript showCR:' process1'.
]
] fork.
p2 := [
10 timesRepeat:[
Transcript show:'here'.
Delay waitForSeconds:0.1.
Transcript show:' is'.
Delay waitForSeconds:0.1.
Transcript showCR:' process2'.
]
] fork.
#terminate,
to the process, or implicit, when the processes code block
(the block which was the receiver of #newProcess or #fork)
leaves (i.e. falls through the last statement).
#suspend,
or indirectly by waiting for some semaphore to be signalled.
The process remains suspended, until it receives a #resume
message.
#resume
message to the waiting process(es).
#yield message,
it gives up control and passes it to the next runnable process
with the same priority, if there is any.
#yield message does nothing.
#yield message can be send by a process itself,
or by another (higher priority) process.
Processor>>yield
or by suspending itself when waiting on some semaphore.
This is called "preemtive scheduling WITHOUT round robin".
The reason for not doing automatic round-robin lies in the above
mentioned dangers when processes are not properly synchronized.
Notice, that ST/X itself is mostly thread-safe,
but applications and your programs may not be so.
Therefore, automatic round robin is disabled by default, but may be enabled as an option, if your application requires it and is prepared for it.
ProcessorScheduler class already contains functions
to timeslice processes.
it creates a new high-priority process, whose execution is controlled by the timer. Since its running at very high priority, this process will always get the CPU for execution, whenever its timer tick expires.
When returning from the timer-wait, that process forces a yield of the running process to allow for other processes within that priority group to execute (for the next timeslice period).
The launchers settings-misc menu includes an item
called "preemtive scheduling" which enables/disables
round-robin (by timeslicing processes).
Timeslicing can also be started by evaluating:
and stopped via:
Processor startTimeSlicing
Processor stopTimeSlicing
Notice, that there is NO warranty concerning the actual usability of this feature; for above (synchronization) reasons, the system could behave strange when the timeslicer is running.
To see the effect of timeslicing, open a ProcessMonitor from the
Launchers utility menu and start some processes which do some long time computation.
For example, you can evaluate (in a workspace):
some 3 or 4 times and watch the processes in the monitor.
[
100 timesRepeat:[3000 factorial]
] forkAt:4
Without timeSlicing, only one of these processes will make any progress;
the others will wait and all of them be executed in sequence.
With timeSlicing on, you will notice that all of them seem to run
(only one of them is actually active at any time).
Dynamic priority handling requires the timeSlicing mechanism
to be active, and the dynamic flag be set:
i.e., to start this mechanism, evaluate:
There is also an entry in the launchers misc-settings dialog,
to enable/disable dynamic process priorities.
Processor startTimeSlicing.
Processor supportDynamicPriorities:true.
For security (and application compatibility), this is not done for all
processes - instead, only processes which return an interval from the
#priorityRange message are affected by this.
The returned interval is supposed to specify the minimum and maximum dynamic
priority of the process.
By default, all created processes have a nil priority range, therefore not
being subject of dynamic scheduling.
The following example, creates three very busy computing processes.
Two of them start with very low priority,
but with dynamic priorities.
The third process will run at a fixed priority.
The first process will never get a priority above any GUI processes,
therefore it will never disturb any views.
|p1 p2 p3|
p1 := [
30 timesRepeat:[
5 timesRepeat:[
3000 factorial
].
Transcript showCR:'p1 made some progress'.
].
Transcript showCR:'p1 finished'.
] forkAt:1.
p1 priorityRange:(1 to:7).
p2 := [
30 timesRepeat:[
5 timesRepeat:[
3000 factorial
].
Transcript showCR:'p2 made some progress'.
].
Transcript showCR:'p2 finished'.
] forkAt:4.
p2 priorityRange:(4 to:9).
p3 := [
30 timesRepeat:[
5 timesRepeat:[
3000 factorial
].
Transcript showCR:'p3 made some progress'.
].
Transcript showCR:'p3 finished'.
] forkAt:6.
The second will eventueally suspend even GUI processes,
in case it does not get a chance to run for a while.
The third process will run at a fixed priority of 6.
Both the first and the second process will eventually suspend the third
process, since their dynamic priority will raise above 6.
(try the example, while evaluating some time consuming operation in
a workspace - at fixed priority 8. In that case, only p2 will make any
progress.).
aProcess setMaximumStackSize:limit
When hit, this limit will raise a smalltalk signal, which can be cought and handled by the
smalltalk exception mechanism. It is even possible to change the limit within the
exception handler and continue execution of the process with more stack
(see examples in "doc/coding").
The default stack limit is set to a reasonably high number (typically some 256k
or more). If your application contains highly recursive code, this default can be
changed with:
Process defaultMaximumStackSize:limit
Now, in the workspace evaluate:
The workspace will no longer respond to keyboard or any other events.
(select the corresponding entry in the processMonitor, and use the
debug function from its popup-menu, to see what the workspace is
currently doing)
Processor activeProcess suspend
You can continue execution of the workspaces process with the processMonitos resume-function (or in the debugger, by pressing the continue button).
All events (keyboard, mouse etc.) are read by a separate process (called
the event dispatcher), which reads the event from the operating system,
puts it into a per-windowgroup event-queue, and notifies the view process
about the arrival of the event (which is sitting on a semaphore, waiting for this
arrival).
For multiScreen operation, one event dispatcher process is executing
for each display connection.
Modal boxes create a new windowgroup, and enter a new dispatch loop on this. Thus, the original views eventqueue (although still being filled with arriving events) is not handled while the modalbox is active (*).
The following pictures should make this interaction clear:
event dispatcher:
modal boxes (and popup-menus) start an extra event loop:
+->-+
^ |
| V
| waits for any input (from keyboard & mouse)
| from device
| |
| V
| ask the view for its windowGroup
| and put event into windowgroups queue
| (actually: the groups windowSensors queue)
| |
| V
| wakeup windowgroups semaphore >*****
| | *
+-<-+ *
* Wakeup !
*
each window-group process: *
*
+->-+ *
^ | *
| V *
| wait for event arrival (on my semaphore) <****
| |
| V
| send the event to the corrsponding controller or view
| | |
+-<-+ |
Controller/View>>keyPress:...
or: Controller/View>>expose...
etc.
+->-+
^ |
| V
| wait for event arrival (on my semaphore)
| |
| V
| send the event to the corrsponding view
| | ^ |
+-<-+ | |
| V
| ModalBox open
| create a new windowgroup (for box)
| |
| V
| +->-+
| ^ |
| | V
| | wait for event arrival (boxes group)
| | |
| | V
| | send the event to the corresponding handler
| | | |
+--- done ? | |
+-<-+ |
keyPress:...
Try evaluating (in a workspace, with timeSlicing disabled) ...
... the system seems dead (read the next paragraphs, before doing this).
[true] whileTrue:[1000 factorial]
Only processes with a higher priority will get control; since the event dispatcher is running at UserInterruptPriority (which is typically 24), it will still read events and put them into the views event queues. However, all view processes run at 8 which is why they never get a chance to actually process the event.
There are two events, which are handled by the event dispatcher itself: a keypress of "CTRL-C" in a view will be recognized by the dispatcher, and start a debugger on the corresponding view-process; a keypress of "CTRL-Y" in a view also stops its processing but does not start a debugger (this is called aborting).
(actually, in both cases, a signal is raised, which could in theory be cought by the view process).
Thus, to break out of the above execution, press "CTRL-C" in the workspace,
and get a debugger for its process.
In the debugger, press either abort (to abort the doIt-evaluation), or
terminate to kill the process and shut down the workspace completely.
If you have long computations to be done, AND you dont like the above
behavior, you can of course perform this computation at a lower
priority. Try evaluating (in the above workspace):
Now, the system is still responding to your input in other views,
since those run at a higher priority (8), therefore suspending the
workspace-process whenever they want to run.
You can also think of the the low-prio processing as being performed in the
background - only running
when no higher prio process is runnable (which is the case whenever
all other views are inactively waiting for some input).
Processor activeProcess priority:4.
[true] whileTrue:[1000 factorial]
Some views do exactly the same, when performing long operations.
For example, the fileBrowser lowers its priority while reading
directories (which can take a long time - especially when directories
are NFS-mounted). Therefore, you can still work with other views
(even other filebrowsers) while reading directories. Try it with
a large directory (such as "/usr/bin").
It is a good idea, to do the same in your programs, if operations take longer than a few seconds - the user will be happy about it. Use the filebrowsers code as a guide.
For your convenience, there is a short-cut method provided by Process,
which evaluates a block at a lower priority (and changes the priority
back to the old value when done with the evaluation).
Thus, long evaluations should be done using a construct as:
You should avoid hardcoding priority numbers into your code, since these
may change (PPS users noticed
that Parcplaces new release 2 uses priorities between 1 and 100),
Processor activeProcess withPriority:4 do:[
10 timesRepeat:[2000 factorial]
]
To avoid breaking your code in case this is changed in ST/X,
the above is better written as:
Processor activeProcess
withPriority:(Processor userBackgroundPriority)
do:[
10 timesRepeat:[2000 factorial]
]
[
10 timesRepeat:[
2000 factorial
]
] forkAt:4
in a workspace, and watch the process monitor.
You will notice, that the workspace is not blocked, but a separate
process has been created. Since it runs at a lower priority, all other
views continue to react as usual.
There is one possible problem with the above background process:
"CTRL-C" or "CTRL-Y" pressed in the workspace will no longer affect the computation (because the computation is no longer under the control of the workspace).To stop/debug a runaway background process, you have to open a Suggested priorities & hints To keep the system responsive, use the following priorities in your programs:
In general, there is seldom any need to raise the priority above the default - except, for example, when handling input (requests) from a Socket which have to be served immediately, even if some user interaction is going on in the meantime (a Database server with a debugging window ?).
If you dont want to manually add yields all over your code, and are not
satisfied with the behavior of your background processes, you may want to
enable timeslicing as described above.
However, you have to care for
the integrity of any shared objects manually.
(BTW: the Transcript in ST/X
is threadsafe; you can use it from any processes, at any priority).
Delay, Semaphore or ProcessorScheduler,
never forget the possibility of external-world interrupts (especially:
timer interrupts). These can in occur at any time, bringing the system
into the scheduler, which could switch to another process as a consequence
of the interrupt.
OperatingSystem blockInterrupts
...
modify the critical data
...
OperatingSystem unblockInterrupts
Since the #blockInterrupts and #unblockInterrupts
implementationdoes not handle nested calls,
you should only unblock interrupts, if they have NOT been blocked in the first place.
#blockInterrupts returns the previous blocking
state - i.e. true, if they have been already blocked.
|wasBlocked|
...
wasBlocked := OperatingSystem blockInterrupts
...
modify the critical data
...
wasBlocked ifFalse:[OperatingSystem unblockInterrupts]
if there is any chance for the code between the block/unblock to return
(i.e. a block return or exception handling),
you also have to add proper unwind actions:
|wasBlocked|
...
wasBlocked := OperatingSystem blockInterrupts
[
...
modify the critical data
...
] valueNowOrOnUnwindDo:[
wasBlocked ifFalse:[OperatingSystem unblockInterrupts]
]
thats a lot to remember; therefore,
for your convenience,
Block offers an "easy-to-use" interface for the above operation:
[
...
modify the critical data
...
] valueUninterruptably.
See the code in Semaphore, Delay and ProcessorScheduler for more examples.
Notice, that no event processing, timer handling or process switching
is done when interrupts are blocked. Thus you should be very careful in
coding these critical regions. For example, an endless loop in such a
region will certainly lock up the smalltalk system.
Also, do not spend too much time in such a region, any processing which takes
longer than (say) 50 milliseconds will have a noticeable impact on the user.
(Usually, it is almost always an indication of a bad design, if you have
to block interrupts for such a long time. In most situations, a critical
region or even a simple Semaphore should be sufficient.)
While interrupts are blocked, incoming interrupts will be registered by
the runtime system and processed (i.e. delivered) at unblock-time.
Be prepared to get the interrupt(s) right after (or even within) the unblock
call.
Also, process switches will restore the blocking state back to how it was when the process was last suspended. Thus, a yield within a blocked interrupt section will usually reenable interrupts in the switched-to process.
It is also possible to enable/disable individual interrupts. See OperatingSystem's
disableXXX and enableXXX methods.
...
anotherProcess interruptWith:[ some action to be evaluated ]
...
This forces anotherProcess to evaluate the block passed to interruptWith:.
If the process is suspended, it will be resumed for the evaluation.
The evaluation will be performed by the interrupted process,
on top of the running or suspended context
(thus a signal-raise, long return, restart or context walkback is possible).
BTW: the event dispatchers "CTRL-C" processing is implemented using exactly this mechanism.
Try:
then:
|p|
p :=[
[true] whileTrue:[
1000 factorial
]
] forkAt:4.
"
to find it easier in the process monitor
"
p name:'my factorial process'.
"
make it globally known
"
Smalltalk at:#myProcess put:p.
or (see the output on the xterm-window, where ST/X has been started):
myProcess interruptWith:[Transcript showCR:'hello'].
or:
myProcess interruptWith:[thisContext fullPrintAll].
finally cleanup (terminate the process) with:
"this brings the process into the debugger"
myProcess interruptWith:[Object errorSignal raise]
As another example, we can catch some signal in the process,
as in:
myProcess terminate.
Smalltalk removeKey:#myProcess.
then send it the signal with:
|p|
p :=[
Object errorSignal catch:[
[true] whileTrue:[
1000 factorial
]
].
Transcript showCR:'process finished gracefully'.
Smalltalk removeKey:#myProcess.
] forkAt:4.
"
to find it easier in the process monitor
"
p name:'my factorial process'.
"
make it globally known
"
Smalltalk at:#myProcess put:p.
The above was shown for demonstration purposes; since process termination
is actually also done by raising an exception (
myProcess interruptWith:[Object errorSignal raise]
Process terminateSignal),
graceful termination is better done by:
then terminate it with:
|p|
p :=[
Process terminateSignal catch:[
[true] whileTrue:[
1000 factorial
]
].
Transcript showCR:'process finished gracefully'.
Smalltalk removeKey:#myProcess.
] forkAt:4.
"
to find it easier in the process monitor
"
p name:'my factorial process'.
"
make it globally known
"
Smalltalk at:#myProcess put:p.
myProcess terminate
ProcessorScheduler offers methods to
schedule timeout-actions. These will interrupt the
execution of a process and force evaluation of a block after some time.
this kind of timed blocks are installed (for the current process) with:
to interrupt other processes after some time, use:
Processor addTimeBlock:aBlock afterSeconds:someTime
there are alternative methods which expect millisecond arguments for short time delays.
Processor addTimeBlock:aBlock for:aProcess afterSeconds:someTime
For example, the autorepeat feature of buttons is done using this
mechanism. Here a timed block is installed with:
Also, animations can be implemented with this feature (by scheduling a block to draw the next
picture in the view after some time delay).
Processor addTimeBlock:[self repeat] afterSeconds:0.1
See ``working with timers & delays'' for more information.
#terminate message.
Technically,
this does not really terminate the process,
but instead raises the ProcessTermination
signal, Process terminateSignal.
Process>>startup method in a browser).
This is called ``soft termination'' of a process.
A ``hard termination'' (i.e. immediate death of the process without any cleanup) can be
done by sending it #terminateNoSignal.
Except for emergency situations (a buggy or looping termination handler),
there should never be a need for this.
An interesting feature of soft termination is that all unwind blocks
(see Block>>valueOnUnwindDo:) are executed - in contrast to a hard terminate,
which will immediately kill the process.
(read: ``context unwinding'')
A variant of the
If the scheduler was hit with this interrupt,
all other process activities are stopped, which implies that other existing
or new views will not be handled while in this debugger (i.e. the debuggers
inspect functions will not work, since they open new inspector views).
In this debugger, either terminate the current process (if you were lucky,
and the interrupt occured while running in the runaway process)
or try to terminate the bad process by evaluating some expression like:
In some situations, the system may bring you into a non graphical
MiniDebugger
(instead of the graphical DebugView).
This happens, if the active process
at interrupt time was a DebugView, or if any unexpected error occurs within the
debuggers startup sequence.
On some keyboards, the interrupt key is labeled different from "CTRL-C".
To decide, which technique to use, the scheduler process asks the OperatingSystem,
if it supports I/O interrupts (via OperatingSystem >> supportsIOInterrupts),
and uses one of the above according to the returned value.
To date, (being conservative) this method is coded to return
false for most systems (even for some, where I/O interrupts do work).
Uou may want to give it a try and enable this feature by changing the
code to return true.
#terminate message is #terminateGroup.
This terminates a process along with all of the subprocesses it created,
unless the subprocess detached itself from its creator by becoming
a process group leader. A process is made a group leader by sending
it the #beGroupLeader message.
In Interrupting a runaway process
In case of emergency (for example, when a process with a priority
higher than UserInterruptPriority loops endless),
you can press
"CTRL-C" in the xterm window, where Smalltalk/X was started.
This will stop the system from whatever it is doing (even the event dispatcher)
and enter a debugger.
If your runaway process was hit, the debugger behaves as if the "CTRL-C"
was pressed in a view (however, it will run at the current priority, so you
may want to lower it by evaluating:
Processor activeProcess priority:8
Your runaway process is of course easier to locate, if you gave it a
distinct name before; in this case, use:
Process allInstances do:[:p |
p priority > 24 ifTrue:[
p id > 1 ifTrue:[ "/ do not kill the scheduler
p terminate
]
]
]
A somewhat less drastic fix is to send it an abortSignal:
Process allInstances do:[:p |
p name = 'nameOfBadProcess'ifTrue:[
p terminate
]
]
Most processes provide a handler for this signal at some save place,
where they are prepared to continue execution. Those without a handler
will terminate.
Therefore, a workspace or browser will return to its event loop,
while other processes may terminate upon receipt of this signal.
Process allInstances do:[:p |
p name = 'nameOfBadProcess'ifTrue:[
p interruptWith:[Object abortSignal raise]
]
]
The MiniDebugger too supports expression evaluation, abort and terminate functions,
however, these have to be entered via the keyboard in the xterm window (where you
pressed the "CTRL-C" before). Type ? (question mark) at
the MiniDebuggers prompt to get a list of available commands.
The exact label depends on the xmodmap and stty-settings.
Try "DEL" or "INTR"
or have a look at the output of the stty
unix command.
However, no input events (keyboard and/or mouse)
are handled while a modalBox is active.
How the scheduler gets control
The scheduling of all smalltalk processes is done by another process, the so called
scheduler process. This process happens to execute at the highest
priority (in current systems: 31).
Whenever the scheduler passes control to another (user- or background)
process, it must make certain that it will regain control (i.e. execute)
whenever a scheduled timeout is about to be handled, or some I/O data arrives
at some stream (to signal corresponding seaphores).
Of particular interest is the display connection,
which must be served whenever input arrives,
to make certain the CTRL-C processing is performed (to stop runaway user processes).
Depending on the OperatingSystem, two mechanism are used:
The first technique leads to less idle CPU overhead, but happens to
not work reliably on all systems.
The second can be used on all systems (especially, some
older Unix versions had trouble in their X-display connection, when I/O interrupts
were used).
If you are still able to CTRL-C-interrupt an endless loop in a workspace,
and timeslicing/window redrawing happens to still work properly,
you can keep the changed code and send a note to cg@exept.de or
info@exept.de; please include the system type and OS release;
we will then add #ifdef'd code to the supportsIOInterrupt method.
Copyright © 1995 Claus Gittinger Development & Consulting
Doc $Revision: 1.19 $ $Date: 1999/10/23 13:23:45 $