[squeak-dev] Refactoring #terminate to get rid of 'cannot return' errors etc.

Jaromir Matas m at jaromir.net
Sun Mar 14 07:58:11 UTC 2021


This is a follow up on the following threads:
[1]
http://forum.world.st/The-Inbox-Kernel-jar-1376-mcz-td5127335.html#a5127336
[2]
http://forum.world.st/The-Inbox-Kernel-jar-1377-mcz-td5127438.html#a5127439


I've noticed resuming a terminated process ends up with "cannot return" and
similar errors. I'd like to offer an alternative approach requiring just a
few tiny changes:

A process is terminated when it suspends itself or some other process does.
When someone resumes a terminated process it continues and eventually fails.
I'd like to propose a terminal method where all processes terminate:

Process>> terminated
        "When I reach this method, I'm terminated.
        Suspending or terminating me is harmless."
       
        thisContext terminateTo: nil.   "sets thisContext sender to nil"
        self suspend.
        ^thisContext restart


This approach would simplify the #isTerminated method and would require just
two little tweaks in the #terminate method:

terminate
        "Stop the process that the receiver represents forever.
         Unwind to execute pending ensure:/ifCurtailed: blocks before
terminating.
         If the process is in the middle of a critical: critical section,
release
it properly."

        | ctxt unwindBlock oldList |
        self isActiveProcess ifTrue:
                [ctxt := thisContext.
                 [ctxt := ctxt findNextUnwindContextUpTo: nil.
                  ctxt ~~ nil] whileTrue:
                        [(ctxt tempAt: 2) ifNil:
                                ["N.B. Unlike Context>>unwindTo: we do not
set complete (tempAt: 2) to
true."
                                 unwindBlock := ctxt tempAt: 1.
                                 thisContext terminateTo: ctxt.
                                 unwindBlock value]].
                "Now all work is done and the process can terminate"
>>>> ^self terminated].

        "Always suspend the process first so it doesn't accidentally get
woken up.
         N.B. If oldList is a LinkedList then the process is runnable. If it
is a
Semaphore/Mutex et al
         then the process is blocked, and if it is nil then the process is
already
suspended."
        oldList := self suspend.
        suspendedContext ifNotNil:
                ["Release any method marked with the <criticalSection>
pragma.
                  The argument is whether the process is runnable."
                 self releaseCriticalSection: (oldList isNil or: [oldList
class ==
LinkedList]).

                "If terminating a process halfways through an unwind, try to
complete that
unwind block first."
                (suspendedContext findNextUnwindContextUpTo: nil) ifNotNil:
                        [:outer|
                         (suspendedContext findContextSuchThat:[:c| c
closure == (outer tempAt:
1)]) ifNotNil:
                                [:inner| "This is an unwind block currently
under evaluation"
                                 suspendedContext runUntilErrorOrReturnFrom:
inner]].
                ctxt := self popTo: suspendedContext bottomContext.
                ctxt == suspendedContext bottomContext ifFalse:
                        [self debug: ctxt title: 'Unwind error during
termination'].
                "Set the receiver's context to Process>>#terminated for the
benefit of
isTerminated."
>>>> ctxt setSender: nil receiver: self method: (Process>>#terminated)
arguments: {}
        ]


When the active process terminates itself it sends itself the #terminated
message parking itself in a "deathtrap". When a process is being terminated
by another process its bottom context set to #terminated, effectively
parking it in the same trap again.

That's it. Now the terminated process may get resumed or terminated again
with no error.


Now, while we're at it we could take care of the processes that terminate
"naturally" by reaching the end of their defining block. We need to refactor
#newProcess to create the terminal bottom context (the trap) and set it as
the initial sender of a newly created process for a block. If the process
finishes without explicit termination or via an exception it'll
automatically park itself in the #terminated facility:

newProcess
        "Answer a Process running the code in the receiver. The process is
not
scheduled.
        Create a new bottom context for Process>>#terminated and make it the
sender
        of the new process for the benefit of Process>>#isTerminated."
       
        | newProcess bottomContext |
        "<primitive: 19>" "Simulation guard"
        newProcess := Process new.
        bottomContext := Context sender: nil receiver: newProcess method:
(Process>>#terminated) arguments: {}.
        newProcess suspendedContext: (self asContextWithSender:
bottomContext).
        newProcess priority: Processor activePriority.
        ^newProcess

The previously used #forContext:priority: had to be inlined because of the
cross reference - the new bottom context refers to the new process and the
new process refers back to the new bottom context.

So at this point all "normal" processes (i.e. those created via newProcess)
will terminate in #terminated regardless of whether they terminated
themselves, were terminated by another party or just reached their closing
square bracket.

There's a group of potentially weird processes created via
forContext:priority: that can choose their own fate and won't in general
follow the above logic. For these cases I left the final condition in
#isTerminate: they are also considered terminated when they reach their last
instruction (and stay there):

isTerminated
	"Answer if the receiver is terminated, or at least terminating, i.e. if one 
	of the following conditions is met:
	(1) the receiver is a defunct process (suspendedContext = nil or pc = nil)
	(2) the receiver is suspended within Process>>terminated, i.e. terminated
	(3) the suspendedContext is the bottomContext and the pc is at the endPC"

	self isActiveProcess ifTrue: [^false].
	^suspendedContext isNil or: [suspendedContext isDead]
	or: [suspendedContext methodClass == Process 
		and: [suspendedContext selector == #terminated]]	
>>>	or: [suspendedContext isBottomContext 
		and: [suspendedContext atEnd]]

Examples of such processes:

p := Process forContext: [Processor activeProcess suspend] asContext
priority: Processor activePriority +1. p resume

Process forContext: [] asContext priority: 40


All tests are green but I'm aware this is just an idea that would have to be
tested thoroughly against the VM and the debugger.

Do you think this is something worth implementing though?

For me, the code is more readable, process termination more orderly, no need
for checking whether a process is terminated to avoid "cannot return"
errors, the terminated processes are easily trackable and identifiable while
debugging etc.

I'm enclosing a changeset with the proposed changes:  Refactor_#terminate.cs
<http://forum.world.st/file/t372955/Refactor_%23terminate.cs>  

Thanks,




-----
^[^ Jaromir
--
Sent from: http://forum.world.st/Squeak-Dev-f45488.html


More information about the Squeak-dev mailing list