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. With Unix processes, communication must be done via explicit 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 accessible from the parent process. (This is also true for other child processes).On some systems, which do support native threads, Smalltalk processes are mapped to native threads. However, ST/X currently enforces and makes certain, that only one of them executes at any time.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 returns a new process object, which is NOT yet scheduled
for execution (i.e. it does not start execution).
To schedule the new process for execution, send it a #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.
Each process has accociated with it a process priority, which controls
execution in case more than one process is ready to run at the same time.
By default, the new process has the same processes priority as
its creating processes (the parent 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
become invalid.
SharedQueue
- provides a safe implementation of a queue
SharedCollection
- provides a safety wrapper for any other collection
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. Semaphores for mutual
exclusions can be acquired and held while some piece of code - the so called "critical region" - is to be executed.
Only one process can hold on a given exclusive semaphore at any time.
If a process wants to acquire the semaphore which is held by another process at
that time, the acquiring process is set to sleep until the owning process
releases the semaphore.
The following code-fragment creates such a semaphore, and wraps the read/write access code into so called critical regions:
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.
doSomeComplexOperation
accessLock critical:[
...
update some state
...
self someOtherOperation
...
update more state
...
].
and:
someOtherOperation
accessLock critical:[
...
update some state
...
].
With a "normal" semaphore, execution of doSomeComplexOperation leads to a
deadlock situation, when the already acquired semaphore is tried to be acquired again.
RecursionLock
should
be used; this behaves like a regular semaphore except that it allows
the owning process (but only the owner) to reenter a critical region.
To use a recursion lock, change the above code from
Semaphore forMutualExclusion
into
RecursionLock new
or
RecursionLock forMutualExclusion
.
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 (the critical
method ensures thus).
#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.
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
or by going into a sleep for some time (see Delay class).
The process remains suspended, until it receives a #resume
message.
#resume
message to the waiting process(es). If it waits for I/O, the runtime system's
I/O handling code will trigger the semaphore signalling.
#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 sent by a process to itself,
or by another (higher priority) process to force it to yield.
This is called "preemtive scheduling WITHOUT round robin".
Notice, that ST/X itself is mostly thread-safe, the windowing
system, shared collections such as dependencies and process lists are
all protected by critical regions.
However applications and your programs may not be so.
Therefore, automatic round robin is initially disabled by default, but later enabled controlled by a settings option. The startup file as delivered now has this setting enabled. If your application is not prepared for it, it should be disabled there.
it creates a new high-priority process, whose execution is controlled by the timer. Since it is running at very high priority, this process will always get the CPU for execution, whenever it's timer tick expires.The launcher's settings-misc menu includes an item called "Preemtive Scheduling" which enables/disables round-robin mechanism.
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).
|
|
Notice, that there is NO warranty concerning the actual usability of this feature; for above (synchronization) reasons, some applications could behave strange when the timeslicer is running. Especially those ported from other Smalltalk dialects, which do not offer timeslicing may not include appropriate locking mechanisms, as they might have been never designed to be thread safe. For example, many assume that a window redraw operation is executed uninterrupted, which is no longer true, when time slicing is enabled.
To see the effect of timeslicing, open a ProcessMonitor
from the
Launcher's
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.
Dynamic priorities are most useful for background actions, where you want to make sure that they do not normally interfere with the user interface too much, but are guaranteed to make progress, even when other processes do heavy cpu centric processing. Typically, background communication protocols, print jobs or other house-keeping tasks benefit from a dynamic process priority.
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 1Mb). If your application contains highly recursive code, this default can be changed with:
|
Processor currentProcess environmentAt:#name put:someValue
and the value fetched via:
Processor currentProcess environmentAt:#name
The "environmentAt:" will raise an error if the variable was undefined.
As an alternative, use "threadVariableValueOf:", which returns nil for undefined variables.
If you need a variable to be only valid during the execution of some code,
use
The last one being recommended, as it makes sure that the variable gets removed (released)
when no longer needed.
Processor currentProcess
withThreadVariable:#name boundTo:someValue
do:[
...
(Processor currentProcess threadVariableValueOf:#name)
...
]
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 view's 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 windowgroup's queue
| (actually: the group's windowSensor's 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 view's 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"(or "CTRL-." depending on your setup)
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).
Notice: in the current ST/X release,
the keyboard mapping was changed to map CTRL-Y to "redo".
Now, you have to press CTRL-. and then click on the "abort" button.
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"/"CTRL-." 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 (closes the workspace).
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):
|
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).
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 FileBrowser's 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 ProcessMonitor, select the background process and apply the processMonitors terminate, abort or debug menu functions.
|
#closeRequest
or a view's #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 should enable timeslicing as described above. However, you have to care for the integrity of any shared objects manually, and fix your code to deal with concurrent accesses (i.e. add critical regions, where required).
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. This may even happen while running at high priority.
|
#blockInterrupts
and #unblockInterrupts
implementation does not handle nested calls,
you should only unblock interrupts, if they have NOT been blocked in the first place.
To do so, #blockInterrupts
returns the previous blocking
state - i.e. true
, if they had been already blocked before.
|
|
Block
offers an "easy-to-use" interface (syntactic sugar) 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. Therefore, 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
in the interrupt action block and will be executed on behalf of the interrupted
processes stack - not the caller's stack. Therefore, this is a method of
injecting a piece of code into any other process at any time.
BTW: the event dispatcher's "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 a TerminateProcessRequest exception.
Of course, this signal can be cought or otherwise handled by the process;
especially to allow for the execution of cleanup actions,
as shown in the above example
(see the Process » startup
method in a browser).
This process termination is called ``soft termination'', because the affected process still has a chance to gracefully perform any cleanup in unwind blocks or by providing a handler for the exception.
A ``hard termination'' (i.e. immediate death of the process without any cleanup)
can be enforced by sending it #terminateNoSignal
.
Except for emergency situations (a buggy or looping termination handler),
there should never be a need for this, because it may leave semaphores or
locks in a state which prevents further access to shared state.
Often, you will have to care for those leftover locks in a
ProcessMonitor
or SemaphoreMonitor afterwards.
So the most distinguishing aspect of soft termination is that all unwind blocks
(see Block » valueOnUnwindDo:
) are executed - in contrast to a hard terminate,
which immediately kills the process.
(read: ``context unwinding'')
#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.
(Notice: this may be impossible to do on the Windows operating system which does not attach a console to ".exe" programs. Therefore, for Windows, ST/X is deployed also as a ".com" application, which always opens a controlling console. During development, it is therefore prefereable to use the "stx.com" executable.)
The interrupt will ST/X in whatever it is doing (even the event dispatcher) and enter a debugger.
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).
You 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 at 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, all rights reserved
<cg at exept.de>