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:
|
Forking a process at low priority:
|
Creating, setting its name and resuming a process:
|
#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
Semaphores
- especially Semaphores for mutual exclusive access.
setup:
|
|
|
"Semaphore forMutualExclusion"
expression creates a
semaphore, which provides safe execution of critical regions.
#critical:
message,
which executes the argument block while asserting, that only one process
is ever within a region controlled by the semaphore.
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/recursionLock is automatically released - there is no need for the programmer to care for this.
#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.
#enter
and #exit
methods - so the critical region can be left anywhere else from where it was
entered.
Using a monitor, the above example is written as:
|
SharedQueues
,
(which are basically implemented much like above code).
|
|
|
synchronized
" keyword.
Using the synchronized message, the above shared access code becomes:
writer:
|
|
The smalltalk classes could be rewritten to add interlocks at every possible
access in those containers (actually, many other classes such as the complete
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.
two processes, NOT synchronized (results in corrupted output):
synchronized by a critical region:
synchronized using the synchronized (syntactic sugar) method:
synchronized by a Monitor:
#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 it's running at very high priority, this process will always get the CPU for execution, whenever it's 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:
|
|
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):
|
Dynamic priority handling requires the timeSlicing mechanism
to be active, and the dynamic flag be set:
i.e., to start this mechanism, evaluate:
|
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.
|
"doc/coding"
).
The default stack limit is set to a reasonably number (typically 64k under Unix and 2Mb under wind32). If your application contains highly recursive code, this default can be changed with:
|
Now, in the workspace evaluate:
|
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) ...
|
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 don't like the above behavior, you can of course perform this computation at a lower priority. Try evaluating (in the above workspace):
|
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:
|
|
|
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
|myProcess|
...
myProcess := [
... whatever ...
] forkAt:4.
...
myProcess terminate
#closeRequest
or a views #destroy
method).
To allow easier identification of your subprocesses in the process monitor, you can assign a name to a process:
|
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 don't 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.
|
#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.
|
|
Block
offers an "easy-to-use" interface for the above operation:
|
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.
|
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:
|
|
|
|
|
|
Process terminateSignal
),
graceful termination is better done by:
|
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:
|
|
For example, the autorepeat feature of buttons is done using this mechanism. Here a timed block is installed with:
|
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 #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 ST/X, all GUI processes are process leaders by default.
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).
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:
|
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.
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.
On some keyboards, the interrupt key is labeled different from "CTRL-C".
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.
|
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.
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
<cg@exept.de>