Squeak SmalltalkJoker Squeak Smalltalk : System : prevnext Concurrency Process Switching Round Robin

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.