[squeak-dev] The Inbox: Kernel-jar.1380.mcz

Jaromir Matas m at jaromir.net
Sun Mar 7 10:42:06 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
	...
	suspendedContext isBottomContext and: [suspendedContext atEnd]

Examples:

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.

The enclosed Kernel file is intended for your consideration and testing
only; it has not been thoroughly tested against any tools or the VM.

Thanks,



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


More information about the Squeak-dev mailing list