Concurrent Programming
Concurrent Programming
Pharo
The licensor cannot revoke these freedoms as long as you follow the license terms.
Under the following conditions:
Attribution. — You must give appropriate credit, provide a link to the license, and
indicate if changes were made. You may do so in any reasonable manner, but
not in any way that suggests the licensor endorses you or your use.
NonCommercial. — You may not use the material for commercial purposes.
NoDerivatives. — If you remix, transform, or build upon the material, you may not
distribute the modified material.
No additional restrictions. — You may not apply legal terms or technological measures
that legally restrict others from doing anything the license permits.
[Link]
Any of the above conditions can be waived if you get permission from the copyright
holder. Nothing in this license impairs or restricts the author’s moral rights.
Layout and typography based on the sbabook LATEX class by Damien Pollet.
Contents
Illustrations iii
2 Semaphores 15
2.1 Understanding semaphores . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.2 An example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.3 wait and signal interplay . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.4 A key question about signal . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.5 Prearmed semaphore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.6 Semaphore forMutualExclusion . . . . . . . . . . . . . . . . . . . . . . . . 22
2.7 Deadlocking semaphores . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.8 Mutex . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.9 Implementation: the language perspective . . . . . . . . . . . . . . . . . . 24
2.10 Implementation: the VM perspective . . . . . . . . . . . . . . . . . . . . . 25
2.11 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3 Scheduler’s principles 29
3.1 Revisiting the class Process . . . . . . . . . . . . . . . . . . . . . . . . . 29
3.2 Looking at some core process primitives . . . . . . . . . . . . . . . . . . . 30
3.3 Priorities . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.4 signal and preemption . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.5 Understanding yield . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.6 yield illustrated . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
i
Contents
ii
Illustrations
1-1 Process states: A process (green thread) can be in one of the following
states: runnable, suspended, executing, waiting, terminated. . . . . . . . 5
1-2 The scheduler knows the currently active process as well as the lists of
runnable processes based on their priority. . . . . . . . . . . . . . . . . . . 8
2-1 The semaphore protects a resource: P0 is using the resource, P1...2 are
waiting for the resource. . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2-2 The process P4 wants to access the resource: it sends the message wait
to the semaphore. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2-3 P4 is added to the waiting list. . . . . . . . . . . . . . . . . . . . . . . . . . 17
2-4 P0 has finished to use the resource: it send the message signal it to the
semaphore. The semaphore resumes the first pending process. . . . . . . 17
2-5 The resumed process, P1, is added to the scheduled list of process of the
ProcessScheduler: it becomes runnable. . . . . . . . . . . . . . . . . . . . 17
iii
Illustrations
This book describes the low-level abstractions available in Pharo for concur-
rent programming. It explains pedagogically different aspects. Now, if you
happen to create many green threads (called Process in Pharo) we suggest
that you have a look at TaskIt. TaskIt is an extensible library to manage con-
current processing at a higher-level of abstractions. You should definitively
have a look at it.
We did several iterations and integrated many feedback and we want to
thank all the revieiwes. Still we are interesting in typos, english corrections,
potential mistakes or any kind of feedback.
You can simply contact us at [Link]@[Link]
18 February 2020.
Thanks DiagProf, Eliot Miranda, Sven Van Caekenberghe, and Ben Coman for
their feedback, ideas, suggestions. Than you again. Special thanks to Diag-
Prof for his patience fixing typos. Special thanks to Ben Coman for the great
examples.
1
CHAPTER 1
Concurrent programming in
Pharo
Pharo is a sequential language since at one point in time there is only one
computation carried on. However, it has the ability to run programs concur-
rently by interleaving their executions. The idea behind Pharo is to propose
a complete OS and as such a Pharo run-time offers the possibility to execute
different processes in Pharo lingua (or green threads in other languages)
that are scheduled by a process scheduler defined within the language.
Pharo’s concurrency is priority-based preemptive and collaborative. It is pre-
emptive because a process with higher priority interrupts (preempts) pro-
cesses of lower priority. It is collaborative because the current process should
explicitly release the control to give a chance to the other processes of the
same priority to get executed by the scheduler.
In this chapter we present how processes are created and their lifetime. We
will show how the process scheduler manages the system.
In a subsequent chapter we will present the semaphores in details and revisit
scheduler principles then later we will present other abstractions such as
Mutex, Monitor and Delay.
3
Concurrent programming in Pharo
Pharo, processes are what is usually called a (green) thread or fiber in other
languages. They have their own execution flow but share the same memory
space and use concurrent abstractions such as semaphores to synchronize
with each other.
We see that the two processes run concurrently, each outputting a number
at a time and not producing two numbers in a row. We also see that a process
has to explicitely give back the execution control to the scheduler using the
expression Processor yield. We will explain this with more details in the
following. Let us look at what a process is.
1.3 Process
In Pharo, a process (green thread) is an object as anything else. A process is
an instance of the class Process. Pharo follows the Smalltalk naming and
from a terminology point of view, this class should be called a Thread as in
other languages. It may change in the future.
A process is characterized by three information:
• A process has a priority (between 10 lowest and 80 highest). Using this
priority, a process will preempt other processes having lower priority
and it will be managed by the process scheduler in the group of process
with the same priority as shown in Figure 1-2.
4
1.4 Process lifetime
executing wait*
waiting
* sent to a Semaphore
Figure 1-1 Process states: A process (green thread) can be in one of the following
states: runnable, suspended, executing, waiting, terminated.
5
Concurrent programming in Pharo
To be executed, this process should be scheduled and added to the list of sus-
pended processes managed by the process scheduler. This is simply done by
sending it the message resume.
In the inspector opened by the previous expression, you can execute self
resume and then the process will be scheduled. It means that it will be added
to the priority list corresponding to the process priority of the process sched-
uler and that the process scheduler will eventually schedule it.
self resume
Note that by default the priority of a process created using the message new-
Process is the active priority: the priority of the active process.
6
1.6 First look at ProcessorScheduler
the message resume that we saw previously. We can also terminate a process
using the message terminate. A terminated process cannot be scheduled
anymore. The process scheduler terminates the process once its execution is
done.
| pr |
pr := [ :max |
1 to: max do: [ :i | i crTrace ] ] newProcessWith: #(20).
pr resume.
pr isTerminated
>>> true
7
Concurrent programming in Pharo
ProcessorScheduler
quiescentProcessLists
activeProcess
<<uniqueInstance>> priority 80
quiescentProcessLists P3 P2 P1
Processor
activeProcess priority i
P0 Pp Pk Pz
priority 10
pending process lists
Figure 1-2 The scheduler knows the currently active process as well as the lists
of runnable processes based on their priority.
The scheduler maintains priority lists, also called run queues, of pending pro-
cesses as well as the currently active process (See Figure 1-2). To get the
running process, you can execute: Processor activeProcess. Each time
a process is created and scheduled it is added at the end of the run queue
corresponding to its priority. The scheduler will take the first process and
executes it until a process of higher priority interrupts it or the process gives
back control to the processor using the message yield.
The scheduler has process priorities from 10 to 80. Only some of these are
named. The programmer is free to use any priority within that range that
they see fit. The following table lists all the predefined priorities together
with their numerical value and purpose.
8
1.8 ProcessScheduler rules
9
Concurrent programming in Pharo
It shows that the the process of priority 14 is executed prior to the one of
priority 13.
We get the following output, which display the priority of the executing pro-
cess.
@14 1
@14 1
@14 1
@13 2
@13 2
@13 2
@12 3
@12 3
@12 3
10
1.10 Yielding the computation
What you should see is that the message yield was sent, but the scheduler
rescheduled the process of the highest priority that did not finish its execu-
tion. This example shows that yielding a process will never allow a process of
lower priority to run.
This is normal since the processes have the same priority. They are sched-
uled and executed one after the other. p1 executes and displays its output.
Then it terminates and p2 gets the control and executes. It displays its out-
put and get terminated.
During the execution of one of the processes nothing forces it to relinquish
computation. Therefore it executes until it finishes. It means that if a pro-
cess has an endless loop it will not release the execution except if it is pre-
empted by a process of higher priority (see Chapter scheduler’s principle).
11
Concurrent programming in Pharo
Using yield
We modify the example to introduce an explicit return of control to the pro-
cess scheduler.
| p1 p2 |
p1 := [ 1 to: 10 do: [:i| i trace. ' ' trace. Processor yield ] ]
fork.
p2 := [ 11 to: 20 do: [:i| i trace. ' ' trace. Processor yield ] ]
fork.
We obtain the following trace showing that each process gave back the con-
trol to the scheduler after each loop step.
1 11 2 12 3 13 4 14 5 15 6 16 7 17 8 18 9 19 10 20
Summary
Let us revisit what we learned in this chapter.
• Processes with the same priority are executed in the same order they
were added to scheduled process list. In fact processes within the same
priority should collaborate to share the execution amongst themselves.
In addition, we should pay attention since a process can be preempted
by a process of higher prioriy, the semantics of the preemption (i.e.,
how the preempted process is rescheduled) has an impact on the pro-
cess execution order. We will discuss this is in depth in following chap-
ters.
• Processes should explicitly give back the computation to give a chance
to other pending processes of the same priority to execute. The same
remark as above works here too.
• A process should use Processor yield to give an opportunity to run
to the other processes with the same priority. In this case, the yielding
process is moved to the end of the list to give a chance to execute all
the pending processes (see below Scheduler’s principles).
12
1.12 Conclusion
1.12 Conclusion
We presented the notion of process (green thread) and process scheduler.
We presented briefly the concurrency model of Pharo: preemptive and col-
laborative. A process of higher priority can stop the execution of processes
of lower ones. Processes at the same priority should explicit return control
using the yield message.
In the next chapter we explain semaphores since we will explain how the
scheduler uses delays to performing its scheduling.
13
CHAPTER 2
Semaphores
15
Semaphores
P0
Figure 2-1 The semaphore protects a resource: P0 is using the resource, P1...2
are waiting for the resource.
P4 wait
P0
Figure 2-2 The process P4 wants to access the resource: it sends the message
wait to the semaphore.
Details
A semaphore will only release as many processes from wait messages as it
has received signal messages. When a semaphore receives a wait message
for which no corresponding signal has been sent, the process sending the
wait is suspended. Each semaphore maintains a linked-list of suspended pro-
cesses, and releases them on a first–in first–out basis.
16
2.2 An example
P0
signal
P0
P0 does not
P4 P3 P2 P1 use the resource
resume
Figure 2-4 P0 has finished to use the resource: it send the message signal it to
the semaphore. The semaphore resumes the first pending process.
P1 Pp Pk Pz
Processor
priority x
pending process lists
Figure 2-5 The resumed process, P1, is added to the scheduled list of process of
the ProcessScheduler: it becomes runnable.
2.2 An example
Before continuing, let us play with semaphores. Open a transcript and in-
spect the following piece of code: It schedules two processes and make them
both wait on a semaphore.
17
Semaphores
| semaphore |
semaphore := Semaphore new.
What you see is that the two processes stopped. They did not finish their job.
When a semaphore receives a wait message, it suspends the process sending
the message and adds the process to its pending list.
Now in the inspector on the semaphore execute self signal. This sched-
ules one of the waiting process and one of the job will finish its task. If we do
not send a new signal message to the semaphore, the second waiting pro-
cess will never be scheduled.
18
2.4 A key question about signal
19
Semaphores
Here the higher priority process (p2) produces a trace, signals the semaphore
and finishes. Then the lower priority process produces a trace, waits and
since the semaphore has been signalled, it executes and terminates.
@30 Process 2a up to signalling semaphore
@30 Process 2b continues and terminates
@20 Process 1a waits for signal on semaphore
@20 Process 1b received signal and terminates
Here the higher priority process (p1) produces trace and waits on the semaphore.
p2 is then executed: it produces a trace, then signals the semaphore. This
signal message reschedules p1 and since it is of higher priority, it is exe-
cuted first preempting (p2) and it terminates. Then p2 terminates.
@30 Process 1a waits for signal on semaphore
@20 Process 2a up to signalling semaphore
@30 Process 1b received signal and terminates
@20 Process 2b continues and terminates
There is subtle point that the second example does not illustrate but that is
worth that we discuss: while the lowest priority process signaled the semaphore
it gets preempted by the higher priority ones. This raises the question of
what it the process to be rescheduled after preemption. The example does
not show it because we got only one process of priority 20. We will go over
this point in the next Chapter.
20
2.5 Prearmed semaphore
Example
Let us modify slightly the previous example. We send a signal message
to the semaphore prior to creating the processes. The semaphore is then
prearmed.
| trace semaphore p1 p2 |
semaphore := Semaphore new.
semaphore signal.
trace := [ :message | ('@{1} {2}' format: { Processor
activePriority. message }) crTrace ].
p1 := [
trace value: 'Process 1a waits for signal on semaphore'.
semaphore wait.
trace value: 'Process 1b received signal and terminates' ]
forkAt: 30.
p2 := [
trace value: 'Process 2a up to signalling semaphore'.
semaphore signal.
trace value: 'Process 2b continues and terminates' ] forkAt: 20.
This example illustrates that a process does not have to systematically wait
on a semaphore.
This is important to make sure that on certain concurrency synchronisation,
all the processes are waiting, while the first one could do its task and send a
signal to schedule others.
We can ask a semaphore whether if it is prearmed using the message isSig-
naled.
21
Semaphores
22
2.8 Mutex
Mutexes (also named RecursionLock) solve this problem. This is why a Mutex
and a Semaphore are not interchangeable. So let’s see what is a Mutex.
2.8 Mutex
A Mutex (MUTual EXclusion) is a semaphore with more information: the cur-
rent process running held in the owner instance variable. As such a Mutex
is an object that protects a shared resource. A mutex can be used when two
or more processes need to access a shared resource concurrently. A Mutex
grants ownership to a single process and will suspend any other process try-
ing to aquire the mutex while in use. Waiting processes are granted access
to the mutex in the order the access was requested. An instance of the class
Mutex will make sure that only one thread of control can be executed simul-
taneously on a given portion of code using the message critical:.
The same code gets blocked on a deadlock with a semaphore. A Mutex and a
semaphore are not interchangeable from this perspective.
Mutex implementation
Object subclass: #Mutex
instanceVariableNames: 'semaphore owner'
classVariableNames: ''
package: 'Kernel-Processes'
The initialize method makes sure that the semaphore is prearmed for
mutual exclusion. Remember it means that the first waiting process will di-
rectly proceed and not get added to the waiting list.
Mutex >> initialize
super initialize.
semaphore := Semaphore forMutualExclusion
The key method is the method critical:. It checks if the owner of the mu-
tex is the current thread. In such case it executes the protected block, and
return. Else it means that the process waits on the critical section and when
23
Semaphores
the semaphore resumes it it sets the process as owner of the section and
makes sure that the owner is reset once the critical section is passed through.
Mutex >> critical: aBlock
"Evaluate aBlock protected by the receiver."
| activeProcess |
activeProcess := Processor activeProcess.
activeProcess == owner ifTrue: [ ^aBlock value ].
^ semaphore critical: [
owner := activeProcess.
aBlock ensure: [ owner := nil ]]
Pharo’s implementation.
A semaphore keeps a number of excess signals: the amount of signals that
did not lead to schedule a waiting process. The message wait and signal
maintain this information: as the implementations below show it, a signal
will increase the excess number and a wait will decrease it.
If the number of waiting processes on a semaphore is smaller than the num-
ber allowed to wait, sending a wait message is not blocking and the process
continues its execution. On the contrary, the process is stored at the end of
the pending list and we will be scheduled when the semaphore will have re-
ceived enough signals.
The fact that the semaphore waiting list is a linked list has an impact on the
semaphore semantics. It makes sure that waiting processes are managed in a
first in first out manner.
While conceptually a semaphore has a list and a counter. At the Pharo im-
plementation level, the class Semaphore inherits from the class LinkedList,
so the waiting process list is ’directly’ the semaphore itself. Since Process
inherits from Link (elements that can be added to linked list), they can be di-
rectly added to the semaphore without being wrapped by an element object.
This is a simplification for the virtual machine.
Here is the implementation of signal and wait in Pharo.
24
2.10 Implementation: the VM perspective
Signal implementation.
The signal method shows that if there is no waiting process, the excess sig-
nal is increased, else when there are waiting processes, the first one is sched-
uled (i.e., the process scheduler resumes the process).
Semaphore >> signal
"Primitive. Send a signal through the receiver. If one or more
processes
have been suspended trying to receive a signal, allow the first
one to
proceed. If no process is waiting, remember the excess signal."
<primitive: 85>
self primitiveFailed
"self isEmpty
ifTrue: [excessSignals := excessSignals+1]
ifFalse: [Processor resume: self removeFirstLink]"
Wait implementation.
The wait method shows that when a semaphore has some signals on excess,
waiting is not blocking, it just decreases the number of signals on excess.
On the contrary, when there is no signals on excess, then the process is sus-
pended and added to the semaphore waiting list.
Semaphore >> wait
"Primitive. The active Process must receive a signal through the
receiver
before proceeding. If no signal has been sent, the active Process
will be
suspended until one is sent."
<primitive: 86>
self primitiveFailed
"excessSignals > 0
ifTrue: [excessSignals := excessSignals - 1]
ifFalse: [self addLastLink: Processor activeProcess suspend]"
25
Semaphores
As we saw previously two primitives are defined: one for wait and one for
signal.
StackInterpreter class >> initializePrimitiveTable
...
"Control Primitives (80-89)"
(85 primitiveSignal)
(86 primitiveWait)
...
We see that the wait primitive checks the number of signal of the semaphore.
When such number is positive, it is decreased and the process is not sus-
pended. On the contrary, it grabs the active process, adds it to the semaphore
list and give back the control to the highest process.
InterpreterPrimitives >> primitiveWait
| sema excessSignals activeProc |
sema := self stackTop. "rcvr"
excessSignals := self fetchInteger: ExcessSignalsIndex ofObject:
sema.
excessSignals > 0
ifTrue:
[self storeInteger: ExcessSignalsIndex ofObject: sema
withValue: excessSignals - 1]
ifFalse:
[activeProc := self activeProcess.
self addLastLink: activeProc toList: sema.
self transferTo: self wakeHighestPriority]
Here if the semaphore list is empty, the signal primitive is incrementing the
signal count of the semaphore. Else, the first pending process is resumed.
StackInterpreter >> synchronousSignal: aSemaphore
"Signal the given semaphore from within the interpreter.
Answer if the current process was preempted."
| excessSignals |
(self isEmptyList: aSemaphore) ifTrue:
["no process is waiting on this semaphore"
excessSignals := self fetchInteger: ExcessSignalsIndex ofObject:
aSemaphore.
self storeInteger: ExcessSignalsIndex
ofObject: aSemaphore
withValue: excessSignals + 1.
^false].
26
2.11 Conclusion
We will explain the preemptionYields used in the last line in a future chap-
ter.
2.11 Conclusion
Semaphore is the lowest level synchronisation mechanism. Pharo offers
other abstractions to synchronize such as Mutexes (also named recursion
lock), Monitors, shared queues, and atomic queues.
27
CHAPTER 3
Scheduler’s principles
In this chapter we revisit the way to scheduler works and present some im-
plementation aspects. In particular we show how yield is implemented. The
Pharo scheduler is cooperative, preemptive across priorities, non-preemptive
within priorities, scheduler. But let us start with the class Process.
It shows that while the process is executing the expression self suspend-
ingList is not nil, while that when the process terminates, its suspending
list is nil.
29
Scheduler’s principles
The second example shows that the process suspendedContext is nil when a
process is executing.
Processor activeProcess suspendedContext isNil.
>>> true
Now a suspended process suspended context should not be nil, since it should
have a stack of the suspended program.
([ 1 + 2 ] fork suspend ; suspendedContext) isNotNil
Implementation details.
The class Process is a subclass of the class Link. A link is an element of a
linked list (class LinkedList). This design is to make sure that processes can
be elements in a linked list without wrapping them in a Link instance. Note
that this process linked list is tailored for the process scheduler logic. This
process linked list is for internal usage. If you need a linked link, better uses
another one if you need one.
States
We saw previously the different states a process can be in. We also saw semaphores
which suspend and resume suspended processes. We revisit the different
states of a process by looking its interaction with the process scheduler and
semaphores as shown in 3-1 :
• executing - the process is currently executing.
• runnable - the process is scheduled. This process is in one of the prior-
ity lists of the scheduler. It may be turned into the executing state by
the scheduler.
• terminated - the process ran and finished its execution. It is not man-
aged anymore by the scheduler. It cannot be executed anymore.
• suspended - the process is not managed by the scheduler: This pro-
cess is not in one of the scheduler lists or in a semaphore list. The pro-
cess can become runnable sending it the resume message. This state is
reached when the process received the message suspend.
• waiting - the process is waiting on a semaphore waiting list. It is not
managed by the scheduler. The process can become runnable when the
semaphore releases it.
30
3.2 Looking at some core process primitives
suspend
terminated
P0
signal
an active process
terminate
executing resume
P7
P4 wait
runnable
P3 P2 P1
Pk Pe Pr
<primitive: 88>
| oldList |
myList ifNil: [ ^ nil ].
oldList := myList.
myList := nil.
oldList remove: self ifAbsent: [ ].
31
Scheduler’s principles
^ oldList
<primitive: 87>
self primitiveFailed
Looking at the virtual machine definition shows that the resumed process
does not preempt processes having the same priority and that would be exe-
cuting.
InterpreterPrimitives >> primitiveResume
"Put this process on the scheduler's lists thus allowing it to
proceed next time there is
a chance for processes of its priority level. It must go to the
back of its run queue so
as not to preempt any already running processes at this level. If
the process's priority
is higher than the current process, preempt the current process."
| proc |
proc := self stackTop. "rcvr"
(objectMemory isContext: (objectMemory fetchPointer:
SuspendedContextIndex ofObject: proc)) ifFalse:
[^self primitiveFail].
self resume: proc preemptedYieldingIf: preemptionYields
32
3.3 Priorities
<primitive: 19>
^ Process
forContext:
[ self value.
Processor terminateActive ] asContext
priority: Processor activePriority
3.3 Priorities
A runnable process has a priority. It is always executed before a process of an
inferior priority. Remember the examples of previous chapters:
| trace |
trace := [ :message | ('@{1} {2}' format: { Processor
activePriority. message }) crTrace ].
[3 timesRepeat: [ trace value: 3. Processor yield ]] forkAt: 12.
[3 timesRepeat: [ trace value: 2. Processor yield ]] forkAt: 13.
[3 timesRepeat: [ trace value: 1. Processor yield ]] forkAt: 14.
@14 1
@14 1
@14 1
@13 2
@13 2
@13 2
@12 3
@12 3
@12 3
This code snippet shows that even if processes relinquish execution (via a
message yield), the processes of lower priority are not scheduled before the
process of higher priority got terminated.
33
Scheduler’s principles
p1 := [
trace value: 'Process 1a waits for signal on semaphore'.
semaphore wait.
trace value: 'Process 1b received signal and terminates' ]
forkAt: 30.
p2 := [
trace value: 'Process 2a up to signalling semaphore'.
semaphore signal.
trace value: 'Process 2b continues and terminates' ] forkAt: 20.
Here the higher priority process (p1) produces trace and waits on the semaphore.
p2 is then executed: it produces a trace, then signals the semaphore. This
signal reschedules p1 and since it is of higher priority, it preempts (p2) and it
terminates. Then p2 terminates.
@30 Process 1a waits for signal on semaphore
@20 Process 2a up to signalling semaphore
@30 Process 1b received signal and terminates
@20 Process 2b continues and terminates
Now we add a second process of lower priority to understand what may hap-
pen on preemption.
| trace semaphore p1 p2 p3 |
semaphore := Semaphore new.
trace := [ :message | ('@{1} {2}' format: { Processor
activePriority. message }) crTrace ].
p1 := [
trace value: 'Process 1a waits for signal on semaphore'.
semaphore wait.
trace value: 'Process 1b received signal and terminates' ]
forkAt: 30.
p2 := [
trace value: 'Process 2a up to signalling semaphore'.
semaphore signal.
trace value: 'Process 2b continues and terminates' ] forkAt: 20.
p3 := [
trace value: 'Process 3a works and terminates'. ] forkAt: 20.
This behavior can be surprising. In fact the Pharo virtual machine offers two
possibilities as we will show later. In one, when a preempting process ter-
minates, the preempted process is managed as if an implicit yield happened,
34
3.5 Understanding yield
moving the preempted process to the end of its run queue on preemption
return and scheduling the following pending process. In another one, when
a preempting process terminates, the preempted process is the one that get
scheduled (it does not move at the end of the pending list). By default, Pharo
uses the first semantics.
| semaphore |
semaphore := Semaphore new.
[ semaphore signal ] fork.
semaphore wait
35
Scheduler’s principles
36
3.7 Considering UI processes
P2
P2 P1
processor waiting list
processor waiting list
write: 1
P1 yield
Py1 P2
Py1
s1 wait P1
P1 write: 11
P2 yield
yiel
d
S1 waiting list
P1 P2
s2 wait
P2
P1 Py2 S2 signal P1
Py2
P2
Figure 3-2 Sequences of actions caused by two processes yielding the control to
the process scheduler.
The following code snippet returns false since the forked process got the pri-
ority than the current process and the current process continued its execu-
tion until the end. Therefore the yielded did not get a chance to be modi-
fied.
| yielded |
yielded := false.
[ yielded := true ] fork.
yielded
>>> false
37
Scheduler’s principles
yielded
>>> true
This expression returns true because fork creates a process with the same
priority and the Processor yield expression allows the forked process to
execute.
Now let us change the priority of the forked process to be lower than the
active one (here the active one is the UI process). The current process yields
the computation but since the forked process is of lower priority, the current
process will be executed before the forked one.
| yielded |
yielded := false.
p := [ yielded := true ] forkAt: Processor activeProcess priority -
1.
Processor yield.
yielded
>>> false
The following illustrates this point using the UI process. Indeed when you
execute interactively a code snippet, the execution happens in the UI process
(also called UI thread) with a priority of 40.
| trace semaphore p1 p2 |
semaphore := Semaphore new.
trace := [ :message | ('@{1} {2}' format: { Processor
activePriority. message }) traceCr ].
p1 := [
trace value: 'Process 1a waits for signal on semaphore'.
semaphore wait.
trace value: 'Process 1b received signal and terminates' ]
forkAt: 30.
p2 := [
trace value: 'Process 2a signals semaphore'.
semaphore signal.
trace value: 'Process 2b continues and terminates' ] forkAt: 20.
trace value: 'Original process pre-yield'.
Processor yield.
trace value: 'Original process post-yield'.
The following traces shows that Processor yield does not change the ex-
ecution of higher priority processes. Here the UI thread is executed prior to
the other and yielding does not execute processes of lower priorities.
@40 Original process pre-yield
@40 Original process post-yield
@30 Process 1a waits for signal on semaphore
@20 Process 2a signals semaphore
@30 Process 1b received signal and terminates
@20 Process 2b continues and terminates
38
3.8 About the primitive in yield method
Now if we make the UI thread waiting for small enough time (but long enough
that the other processes get executed), then the other processes are run
since the UI process is not runnable but waiting.
| trace semaphore p1 p2 |
semaphore := Semaphore new.
trace := [ :message | ('@{1} {2}' format: { Processor
activePriority. message }) traceCr ].
p1 := [
trace value: 'Process 1a waits for signal on semaphore'.
semaphore wait.
trace value: 'Process 1b received signal' ] forkAt: 30.
p2 := [
trace value: 'Process 2a signals semaphore'.
semaphore signal.
trace value: 'Process 2b continues' ] forkAt: 20.
When this method is executed, either the primitive puts the calling process
to the back of its run queue, or (if the primitive is not implemented), it per-
forms what we explained earlier and that is illustrated by the Figure 3-2.
39
Scheduler’s principles
Note that all the primitive does is to circumvent having to create a semaphore,
to create, to schedule a process, and to signal and to wait to move a process
to the back of its run queue. This is worthwhile because most of the time a
process’s run queue is empty, it being the only runnable process at that pri-
ority.
| yielded |
yielded := false.
[ yielded := true ] fork.
Processor yield.
yielded
>>> true
40
3.10 Comparing the two semantics
• Step 1. First we create two processes at a lower priority than the active
process and at a priority where there are no other processes. The first
expression will find an empty priority level at a priority lower than the
active process.
• Step 2. Then create two processes at that priority and check that their
order in the list is the same as the order in which they were created.
• Step 3. Set the boolean to indicate that this point was reached and
block on a delay, allowing the processes to run to termination. Check
that the processes have indeed terminated.
| run priority process1 process2 |
run := true.
"step1"
priority := Processor activePriority - 1.
[(Processor waitingProcessesAt: priority) isEmpty] whileFalse:
[priority := priority - 1].
"step2"
41
Scheduler’s principles
Smalltalk vm processPreemptionYields
ifTrue:
"If process preemption yields, process1 will get sent to the
back of the run
queue (give a chance to other processes to execute without
explicitly yielding a process)"
[ self assert: (Processor waitingProcessesAt: priority) first ==
process2.
self assert: (Processor waitingProcessesAt: priority) last ==
process1 ]
42
3.12 Conclusion
"step3"
run := false.
(Delay forMilliseconds: 50) wait.
"Check that they have indeed terminated"
self assert: (Processor waitingProcessesAt: priority) isEmpty
3.12 Conclusion
This chapter presents some advanced parts of the scheduler and we hope
that it gives a better picture of the scheduling behavior and in particular the
preemption of the current running process by a process of higher priority as
well as the way yielding the control is implemented.
43
CHAPTER 4
Some examples of semaphores
at work
4.1 Promise
Sometimes we have a computation that can take times. We would like to
have the possibility not be blocked waiting for it especially if we do not need
immediately. Of course there is no magic and we accept to only wait when
we need the result of the computation. We would like a promise that we
will get the result in the future. In the literature, such abstraction is called a
promise or a future. Let us implement a simple promise mechanism: our im-
plementation will not manage errors that could happen during the promise
execution. The idea behind the implementation is to design a block that
1. returns a promise and will get access to the block execution value
2. executes the block in a separated thread.
4.2 Illustration
For example, [ 1 + 2 ] promise returns a promise, and executes 1 + 2 in
a different thread. When the user wants to know the value of the promise it
sends the message value to the promise: if the value has been computed, it
is handed in, else it is blocked waiting for the result to be computed.
45
Some examples of semaphores at work
The second test, create a promise and shows that when its value is requested
its value is returned.
testPromise
| promise |
promise := [ 1 + 2 ] promise.
self assert: promise value equals: 3
It is difficult to test that a program will be blocked until the value is present,
since it will block the test runner thread itself. What we can do is to make
the promise execution waits on a semaphore before computing a value and
to create a second thread that waits for a couple of seconds and signals semaphore.
This way we can check that the execution is happening or not.
testPromiseBlockingAndUnblocking
| controllingPromiseSemaphore promise |
controllingPromiseSemaphore := Semaphore new.
46
4.4 Implementation
We have in total three threads: One thread created by the promise that is
waiting on the controlling semaphore. One thread executing the control-
ling semaphore and one thread executing the test itself. When the test is
executed, two threads are spawned and the test will first check that the
promise has not been executed and wait more time than the thread control-
ling semaphore: this thread is waiting some seconds to make sure that the
test can execute the first assertion, then it signals the controlling semaphore.
When this semaphore is signalled, the promise execution thread is scheduled
and will be executed.
4.4 Implementation
We define two methods on the BlockClosure class: promise and promiseAt:.
BlockClosure >> promise
^ self promiseAt: Processor activePriority
| promise |
promise := Promise new.
[ promise value: self value ] forkAt: aPriority.
^ promise
We initialize by simply creating a semaphore and setting that the value has
not be computed.
47
Some examples of semaphores at work
Nwo the method value wait on the protecting semaphore. Once it is execut-
ing, it means that the promise has computed its value, so it should not block
anymore. This is why it signals the protecting semaphore before returning
the value.
Promise >> value
"Wait for a value and once it is available returns it"
valueProtectingSemaphore wait.
valueProtectingSemaphore signal. "To allow multiple requests for
the value."
^ value
Finally the method value: stores the value, set that the value has been
computed and signal the protecting semaphore that the value is available.
Note that such method should not be directly use but should only be invoked
by a block closure.
Promise >> value: resultValue
value := resultValue.
hasValue := true.
valueProtectingSemaphore signal
48
4.5 SharedQueue: a nice semaphore example
These two semaphores are used in the methods to access (next) and add el-
ements (nextPut:). The idea is that a read should be blocked when there is
no element and adding an element will enable reading. In addition any mod-
ification of the internal elements should happen within one single process at
the same time.
SharedQueue >> next
| value |
readSynch wait.
accessProtect
critical: [
readPosition = writePosition
ifTrue: [ self error: 'Error in SharedQueue synchronization'.
value := nil ]
ifFalse: [ value := contentsArray at: readPosition.
contentsArray at: readPosition put: nil.
readPosition := readPosition + 1 ]].
^ value
49
Some examples of semaphores at work
Rendez-vous
Now a question is how can be generalize such a behavior so that we can have
two programs that work freely to a point where a part of the other has been
performed.
50
4.6 About Rendez-vous
For example imagine that we have two prisoners that to escape have to pass
a barrier together (their order is irrelevant but they should do it consecu-
tively) and that before that they have to run to the barrier.
The following output is not permitted.
'a running to the barrier'
'a jumping over the barrier'
'b running to the barrier'
'b jumping over the barrier'
51
Some examples of semaphores at work
| aAtBarrier bAtBarrier |
aAtBarrier := Semaphore new.
bAtBarrier := Semaphore new.
{[ 'a running to the barrier' crTrace.
aAtBarrier signal.
bAtBarrier wait.
'a jumping over the barrier' crTrace ]
.
[ 'b running to the barrier' crTrace.
bAtBarrier signal.
aAtBarrier wait.
'b jumping over the barrier' crTrace ]
} shuffled do: [ :each | each fork ]
4.7 Conclusion
We presented the key elements of basic concurrent programming in Pharo
and some implementation details.
52