As a first simple example, we will consider a problem in the domain of searching. Suppose we have an array of 150.000 records with people. We want to find all persons with an age between 18 and 24. We may do this by writing a method that goes through all the records and check the ages. We may also split the task into three tasks and define three activities where each activity handles one third of the records. This should speed up the process, compared to using just one activity.
The basic elements of a parallel system are objects executing activities in parallel – called active objects. Below we show how the search-activity may be split between active objects:
aSimpleSearcher: obj BasicSystem
searcherA: obj BasicProcess
search(1,50000)
searcherB: obj BasicProcess
search(50001,100000)
searcherC: obj BasicProcess
search(10001,150000)
:::
searcherA.start
searcherB.start
searcherC.start
The object aSimpleSearcher
is subclassed from a class BasicSystem
, which is a class that defines abstractions for describing objects that may execute in parallel. One of these abstractions is class BasicProcess
, which may be used as a superclass for objects that execute in parallel. We supply the details of BasicSystem
in a later chapter.
class BasicSystem:
class BasicProcess:
start: :::
:::
:::
The object aSimpleSearcher
contains three searcher-objects, searcherA
, searcherB
, and searcherC
which all are subclassed from BasicProcess
. This means that they may execute in parallel. The execution of the three searcher-objects is started by invocation of the start
method of class BasicProcess
: searcherA.star
t, etc. They all invoke a search
-method on their third of the list of records. The search
-method searches the range of objects within the arguments of search
.
Next we supply the details of the searching activity, that is the method search
– we assume that we have a class Person
with a name
and an age
attribute:
aSimpleSearcher: obj BasicSystem
records: obj Array(150000, Person)
search(first, last: var integer):
for (first):to(last):repeat
if ((18 <= records[inx].age)
and (records[inx].age <= 24)) :then
collector.insert(records[inx])
collector: obj :::
:::
class Person:
name: var String
age: var integer
...
The object aSimpleSearcher
has a local array, records
, which is an array containing 150000
Person
-objects. The method search
searches the part of the array as defined by its parameters first
and last
. If a record matches the search-criteria is found, the Person
-object is added to a collector
-object.
Note: class BasicSystem
is placed within a module as described in chapter later in this chapter. This will require a minor adjustment to this example in order to be able to execute it.
The collector
-object is implemented as a Set
-object:
collector: obj Set(Person)
This looks simple and straightforward, but we have now arrived at one of the major problems in writing parallel programs. Since the three searcher-objects execute in parallel, two or more of them may invoke collector.insert
at the same time. This may imply that result of collector.insert
has an unexpected result. The problem may be the implementation of the insert
method of Set
. As we shall see in a later chapter, insert
manipulates local references in Set
. This may not work if two or more searcher
-objects does this at the same time. illustreres mere? OLM: hvis du har en ide så vil det være fint.
Vi definerer race condition både her og i 12.3 Other issues in parallel programming, så skal koordineres.
Som eksempel real-life race conditions kan bruges en situation med nogle varer som kan sælges af to elleflere sælgere.
Hvis en sælger er igang med at sælge 3 specifikke items, som er den enste af deres slags, så kan det ikke nutte noget at encnde sælger frigang med at sælge de samme 3 items.
Det kan væres booking af hotelværelse, rejser, flysæder, kunstgenstande, etc.
This problem of two or more parallel objects accessing the same objects at the same time is referred to as race conditions, and as said above it is one of the critical issues of parallel programming. In the next section, we introduce a language mechanism that makes it possible to avoid race conditions.
The monitor system
There exists a number of language mechanism that may help avoiding race conditions. In this section we use a monitor to define a safe version of Collector
.
A monitor is an object where it is only possible to invoke at most one method at a given time. That is two or more methods cannot be invoked at the same time. A new class MonitorProcess
is defined to represent parallel activity that makes use of Monitor
objects.
Class Monitor
and class MonitorProcess
are defined as part of the class MonitorSystem
:
class MonitorSystem:
class MonitorProcess: BasicProcess
start: :::
:::
class Monitor:
entry:
:::
:::
Class Monitor
has a local method entry
that must be used as a supermethod of all methods defined within in a subclass of Monitor
. Only one method that has entry
as a supermethod can be executed at a given time. For methods that have entry
as supermethod, only one can be executed at a given time.
Note: As for BasicSystem
, class MonitorSystem
is placed within a module as described in section X and this will also require a minor adjustment to this example in order to be able to execute it.
We may now define the collector
object as a Monitor
by encapsulating the Set
-object within the Monitor
:
collector: obj Monitor
insert(p: ref Person): entry
matches.insert(p)
matches: obj Set(Person)
As can be seen, collector
is subclassed from Monitor
and insert
is a submethod of entry
. This guarantees that at most one invocation of insert
may be executed at a given time even though it may be called in parallel by the three searcher processes.
A MonitorProcess
in a MonitorSystem
can only acces data-items defined locally in the MonitorProcess
object, but it may invoke methods of a Monitor
object. The reason for this is to avoid race conditions if two or more MonitorProcess
object access the same objects at the same time.
In the simple searcher, the objects to be searched are in the global object records
. This objects can thus not be accessed by a MonitorProcess
object. We thus have to organize the records in another way.
In this simple example, we split the records
array object into three array objects and place one in each searcher object. We define a general Searcher
class that contains the records
and the search method of a given MonitorProcess
:
class Searcher: MonitorProcess
records: obj Array(50000, Person)
search(first, last: var integer):
-"-
inner(Searcher)
We may now define the searcher objects as subclassed from Searcher
:
searcherA: obj Searcher
search(1,50000)
searcherB: obj Searcher
search(1,50000)
searcherC: obj Searcher
search(1,50000)
In this way each Searcher
object has its own records
to be searched.
The search
methods defined above is the same as the one defined in the aSimpleSearcher
. The difference is that here it is placed locally to the three searcher
-objects. Each of them has its own search
method although the code is identical and defined in class Searcher
, which is a superclass of all three objects.
We now add a MonitorProcess
-object that presents the results of the searching. Such a MonitorProcess
has to wait for all three Searcher
-objects to have finished searching their part of the objects.
presenter: obj MonitorProcess
waitTermination(searcherA, SearcherB, SearcherC)
collector.scan
console.print("Found" + current.asText)
The method waitTermination
waits until all its arguments searcherA
, SearcherB
, and SearcherC
have terminated execution.
A scan
-method has been added to collector
. It scans though all elements in the set of matches, and execute an inner
for each element:
collector: obj Monitor
-"-
scan:
current: ref Person
matches.scan
this(scan).current := current
inner(scan)
The complete system then consists of the following elements:
aSafeSearcher: obj MonitorSystem
class Searcher: -"-
searcherA: obj Searcher
search(1,50000)
searcherB: obj Searcher
search(1,50000)
searcherC: obj Searcher
search(1,50000)
collector: obj Monitor
insert(P: ref Person): entry
-"-
scan:
-"-
matches: obj Set(Person)
presenter: obj MonitorProcess
-"-
searcherA.start
searcherB.start
searcherC.start
presenter.start
As being said before, most of the examples in this book is for illustrating basic programming and modelling. In this example we have not dealt with how to add content to the records of the Searcher
object. Also organizing the records as done here may not be the best solution in a practical system.
Using a bounded buffer
Computers in general have limited storage with respect to RAM and disk space, and programs have to take this into account.
The next example is also in the domain of searching. We consider a number of clients that makes requests to a server for searching Person
-objects with specific values of its attributes, such as name
and age
above.
The overall structure of this system is:
usingBoundedBuffer: obj MonitorSystem
class Client: MonitorProcess
mkRequest(field: var String, V: var String) -> R: ref Request:
:::
class Request(sendedr: ref Client, field: var String, V: var String):
:::
...
clientA: obj Client("clientA")
:::
clientB: obj Client("clientB")
:::
clientC: obj Client("clientC")
:::
server: obj Monitor
addRequest(R: ref Request): entry
:::
getRequest -> R: ref Request: entry
:::
requests: obj BoundedBuffer(#Request,100)
Searcher: MonitorProcess
decodeAndSearch(R: ref Request):
...
:::
searcherA: obj Searcher("searcherA")
searcherB: obj Searcher("searcherB")
searcherC: obj Searcher("searcherC")
The following code defines a class Client
that may be used to specify the clients of the system:
class Client: MonitorProcess
mkRequest(field, value: var String) -> R: ref Request:
R := Request(this(Client),field, value)
server.addRequest(r)
inner(Client)
class Request(sender: ref Client, field, V: var String):
...
clientA: obj Client
mkRequest("age","18-24")
mkRequest("name","John Smith")
...
clientB: obj Client
mkRequest("age","60-65")
...
clientC: obj Client
...
- Class
Client
describes the general structure of a client submitting request to theserver
. - Class
Request
describes the structure of a request. It contais thesender
of theRequest
, afield
holding the name of the attribute to search for, and a parameterV
holding the value (or interval of values) that must match thefield
. - The method
mkRequest
creates aRequest
and sends it to the server usingserver.addRequest(R)
. - Three sub of
Client
,clientA
,clientB
, andclientC
subclassed fromClient
are declared, each submitting different requests.
We implement the server as a Monitor
that keeps track of the various requests.
server: obj Monitor
addRequest(R: ref Request): entry
request.insert(R)
getRequest -> R: ref Request: entry
R := requests.next
requests: obj BoundedBuffer(100,Request)
:::
- The method
addRequest
, inserts theRequest
R
in the arrayrequests
. - The method
getRequest
is used by a searcher to get a request. The requests-object is of typeBoundedBuffer
, which is a list where a limited number of objects may be stored – in this case 100.
We have to define a new version of a searcher-object to be used in this example since it the searching is more complicated than just searching for a person with an age between 18 and 24.
class Searcher: MonitorProcess
decodeAndSearch(R: ref Request):
...
cycle
R: ref Request
R := server.getRequest
decodeAndSearch(R)
searcherA: obj Searcher
searcherB: obj Searcher
searcherC: obj Searcher
A Searcher
-process retrieves a request from the Server
. It then has to decode the data-items field
and V
to find out what to search for. This is done by the method decodeAndSearch
.
For a request with field = "age"
and V = "60-65"
, it must read the string field
to find out that it is the age
attribute that is to be used in the search. And it must decode the String
“60-65” to find out that the age must be between 60 and 65. Finally the search method above must have the ages to search for defined as parameters. We don’t show the details here.
For a request with field = "name"
and "V = "John Smith"
, a similar decision must be made and another search-method must be written to search for Person
-objects where the name
-attributes has the given value. Again we don’t show the details.
Handling the capacity of the server
As mentioned, the Server
stores the request in the requests
-object, which can hold a maximum of 100 objects. A client trying to add a Request
must therefore check if the buffer is not full, and if it is, wait until some space is available.
The same is the case for a Searcher
-object. When calling getRequest
, the requests
-buffer may be empty and thus no Request
can be returned to the Searcher
. In this case the method getRequest
must wait until a client inserts a Request
.
To handle this, a Monitor
-object has a method wait
that delays execution of an entry
-method until a given condition becomes true:
wait(condition)
We may use wait
in the server
-object as follows:
server: obj Monitor
addRequest(R: ref Request): entry
wait(not requests.full)
request.insert(R)
getRequest -> R: ref Request: entry
wait(not requests.isEmpty)
R := requests.next
requests: obj BoundedBuffer(100,Request)
In the beginning of the addRequest
-method, a wait(not requests.full)
is inserted. The invocation requests.full
returns true if the buffer is full. The method wait simply delays the execution of addRequest
until the buffer is not full. This may happen if a Searcher
-object removes a Request
using getRequest.
In a similar way, a wait(not requests.isEmpty)
is inserted in the beginning of getRequest
. It delays execution of getRequest
until the buffer is not empty.
Organization of BasicSystem and MonitorSystem
In this section, we describe how the classes BasicSystem
and MonitorSystem
are organized in modules . This is of course necessary to understand in order to be able to executed the above programs.
Class BasicSystem
is placed within the module BasicSystemLib
:
BasiSystemLib: obj
class BasiscSystem:
class BasicProcess:
:::
Class MonitorSystem
is similar within a module MonitorSystemLib
:
MonitorSystem: obj BasicSystemLib.BasicSystem
class MonitorSystem:
class MonitorProcess:
:::
class Monitor:
:::
The necessary adjustments to the above examples implies that the programs must be subclassed from class BasicSystem
or class MonitorSystem
.
The program aSimpleSearcher
must thus be subclassed from class BasicSystemLib.BasicSystem
:
aSimpleSearcher: obj BasicSystemLib.BasicSystem
-"-
The program aSafeSearcher
must be subclassed from class MonitorSystemLib.MonitorSystem
:
aSafeSearcher: obj MonitorSystemLib.MonitorSystem
-"-
Finally, usingBoundBuffer
must also be subclassed from class MonitorSystemLib.MonitorSystem
:
usingBoundedBuffer: obj MonitorSystemLib.MonitorSystem
-"-