I'll comment on how the current implementation works, hah I hope I get
most of it correct. I know other on the list are actually responsible
for it's creation. If you load the vmmaker squeak map package you will
find.
Interpreter>>transferTo: aProc
Is the method that actually transfer control between active process to
another, outside of direct control of any Smalltalk code.
This method is called via Interpreter>>primitiveSuspend
Interpreter>>primitiveWait Interpreter>>primitiveYield
Interpreter>>resume:
On Wait and Yield we add the current process to the end of a linked
list based on it's priority, then we wake the first runnable process
based on looking from highest to lowest priority. If there is no
runnable process the VM will quit, with 'scheduler could not find a
runnable process', readers of the list are welcome to read the list
archives to find the change set I managed to get into the update
stream which led to that condition for the first few early adopters
one weekend many years ago.
For Resume: we put the active process to sleep and run the indicated
process if the indicated process has a higher priority.
Suspend is used mostly in the interface by the debugger and
ProcessBrower to control the runability of an indicated process.
This algorithm ensures: a) high priority processes will run first. b)
all process at a given priority level will run in round robin fashion
if process switching occurs.
When a process is resumed depends on what it is doing. It could be
waiting on a semaphore, or on a delay. Because of how resume: works a
process can become runnable but not run if a higher priority task can
run. This means of course a higher priority process that does not
yield control it will block runnable lower priority processes, or
processes at the same priority.
But if for example you have a server application that accepts incoming
requests then forks off work at a lower priority this allows the
higher priority listeners to accept work, then lower priority tasks
perform the actual work, but are prevented from blocking higher
priority process from accepting work. I think someone earlier
commented on forking work at the user background priority to prevent
the UI from locking up.
In general most usages of yield I have seen are in endless while loops
where one polls for work churning CPU cycles as a side effect and
without the yield blocking any other runable processes. For the most
part usage of yield in the image follow this pattern.
"In Theory" there is nothing to prevent you from creating a processor
scheduler that wakes up every N milli-seconds and decides to suspend
or change the priority of the running process and picking a different
process to resume. There would be some question if this is 'safe',
however I do recall in VisualWorks at some point there was an optional
changeset to provide this functionality. 'Safe' here means when does
a process lose control and what does that mean for multi-threading.
That I can't answer, mind if someone could point to a Use case that
would be a plausible failure condition that would be interesting.
When processing switching can occur is of course a question to ask.
This occurs. when a method send is made or when a long
unconditionalJump bytecode is executed in the backwards direction. >
9 bytecodes? on a primitive call if when the call takes > 1 or
perhaps 17ms depending on platform.
The checks are in Interpreter>>checkForInterrupts
I'll note we don't attempt to make decisions every time we encounter
these three conditions, rather we attempt to limit our checking every
2-3 milliseconds with some other decision making to ensure we make the
objective of giving Delay accuracy a ms or less degree of error.
This routine does things like check for the low space,
finalization, polling, interrupt key semaphore and attempt to wake
the waiting processes semaphores if required. Usually one of the
three conditions above occur within a 1 ms timeframe.
I will note because Morphic polls for user UI events every 20 or so
milliseconds at a high priority, this ensures if you run things below
Processor lowIOPriority that a keyboard interrupt will be serviced,
this enables the stopping of most runaway processes.
Killing runaway process that have consumed all the existing memory is
another issue for another day, those become unkillable because the VM
can't service enough bytecodes fast enough to trigger the debugger.
----
Higher priority processes that are in a runnablestate (ie not held
back by a semaphore etc) will take control away from any lower
priority process just as soon as they can. As John said, that
opportunity comes with every message send and backwards branch
bytecode, which is quite frequently. However, if a low priority
process calls a very long running prim then you can be held up for a
while. VW has some mechanism for combating this by ... well some means
or other involving native threads.
Recall that the process switch checking done in the VM only checks for
a runnable _higher_ priority process. Not for other equal or lower
priority ones. Yielding is needed if you want to 'play nice' and give
other same-priority processes a go at the cpu; #yield puts the current
process at the back of the right priority list and gets the one now at
the front. This gives a sort-of round robin effect. It does nothing at
all to help starving low class processes. To give lower priorities a
go you have to suspend anything higher - make them wait on a semaphore
with a short delay for example.
Well I spent a few minutes trying to make a method that locks things
up, but that is difficult
test
| a |
1 to: 100000000 do: [:i | a ].
involves a backwards jump
test
self test
involves a method send. I think any reasonable smalltalk method would
involve a message send or backwards jump at some point. As mentioned
by Tim a long running prim does cause you problems, and as noted in
VW they have a pthread (or similar) based solution to solve the
impact of long running blocking prim calls.
> The preceding list seems to indicate that explicitly coding a yield
> would almost never be necessary.
This has been my experience. The only time I've had to use this is
when I've had a tight fast loop that did no I/O, didn't write or read
to SharedQueues, and didn't wait on any Delays.
> How the VM could get control back, in the absence of
> using native threads (if only internally) is not clear to me in that
> case.
It can't. So we try to not make long-running synchronous primitives.
Instead, we try to design such primitives so that Squeak can keep
running, while the Process that called the primitive is blocked
waiting on a Semaphore. When the task is finished, that Semaphore is
signaled.
The other strategy we use (for sockets and async files) is to call
select() in our main loop, and signal the appropriate Squeak
semaphores when the handles become readable or writable.
As someone mentioned, you can easily make a different policy for
handling process switching between processes at the same priority.
This can be done by a high-priority process interrupting frequently
and then going back to sleep.