Processes
A process can be considered as the executable artifact which animates a session. In this section, we will discuss those statements that rule the execution of processes within the engine. In particular, we will show how to address concurrent execution of processes and synchronization.
Execution modality
The execution modality permits to specify the way a process must be executed within the engine. An process can be executed in three modalities:
- single
- sequential
- concurrent
The syntax of the execution modality is:
execution: single | concurrent | sequential
single
is the default execution modality (so the execution
construct can be omitted), which runs the program behaviour once. sequential
, instead, causes the program behaviour to be made available again after the current instance has terminated. This is useful, for instance, for modelling services that need to guarantee exclusive access to a resource. Finally, concurrent
causes a program behaviour to be instantiated and executed whenever its first input statement can receive a message.
In the `sequential` and `concurrent` cases, the behavioural definition inside the main procedure must be an input statement, thus the executed process always animates a session. Single modality is usually exploited for running scripts because they require to be triggered by command instead of a message reception.
A crucial aspect of processes is that each of them has its own private state, determining variable scoping. This lifts programmers from worrying about race conditions in most cases.
For instance, let us recall the server program given at the end of Communication Ports section. The execution modality of the NewsPaper is concurrent
thus it can support multiple requests from both the script author.ol and user.ol.
from NewsPaperInterface import NewsPaperInterface
service NewsPaper {
execution: concurrent
inputPort NewsPaperPort {
location:"auto:ini:/Locations/NewsPaperPort:file:locations.ini"
protocol: sodep
interfaces: NewsPaperInterface
}
main {
[ getNews( request )( response ) {
response.news -> global.news
}]
[ sendNews( request ) ] { global.news[ #global.news ] << request }
}
}
main{}
and init{}
main and init define the behaviour scope and the initializating one respectively. All the operations of the service must be implemented within the scope main, whereas the scope init is devoted to execute special procedures for initialising a service before it makes its behaviours available. All the code specified within the init{}
scope is executed only once, when the service is started. The scope init is not affected by the execution modality. On the contrary, the code defined in the scope main is executed following the execution modality of the service.
As an example let us consider the newspaper service reported above enriched with a simple init scope where a message is printed out on the console:
from NewsPaperInterface import NewsPaperInterface
from console import Console
service NewsPaper {
execution: concurrent
embed Console as Console
inputPort NewsPaperPort {
location:"auto:ini:/Locations/NewsPaperPort:file:locations.ini"
protocol: sodep
interfaces: NewsPaperInterface
}
init {
println@Console("The service is running...")()
}
main {
[ getNews( request )( response ) {
response.news -> global.news
}]
[ sendNews( request ) ] { global.news[ #global.news ] << request }
}
}
When run the service will print out the following message in the console:
The service is running...
Global variables
Jolie also provides global variables to support sharing of data among different processes. These can be accessed using the global
prefix:
global.myGlobalVariable = 3 // Global variable
myLocalVariable = 1 // Local to this behaviour instance
In the example reporters at this link it is shown the difference between a global variable and a local variable. The server is defined as it follows:
from ServiceInterface import ServiceInterface
from console import Console
service MyService {
embed Console as Console
execution: concurrent
inputPort Test {
location: "socket://localhost:9000"
protocol: sodep
interfaces: ServiceInterface
}
main {
test( request)( response ) {
global.count++
count++
println@Console("global.count:" + global.count )()
println@Console("count:" + count )()
println@Console()()
}
}
}
In the body of the request-response test the global variable global.count and the local variable count are incremented by one for each received message, then their value are printed out on the console. If we call such service ten times the output is:
global.count:1
count:1
global.count:2
count:1
global.count:3
count:1
global.count:4
count:1
global.count:5
count:1
global.count:6
count:1
global.count:7
count:1
global.count:8
count:1
global.count:9
count:1
global.count:10
count:1
It is worth noting that the global variable keeps its value independently from the executiing instance, thus it can be incremented each time a new session is executed. On the other hand the local variable is fresh each time.
Synchronisation
Concurrent access to global variables can be restricted through synchronized
blocks, similarly to Java:
synchronized( id ){
//code
}
The synchronisation block allows only one process at a time to enter any synchronized
block sharing the same id
.
As an example, let us consider the service reported at this link
The register service has a concurrent execution and exposes the register
request-response operation. register
increments a global variable, which counts the number of registered users, and sends back a response to the client. A sleep call to time service, simulates the server side computation time.
RegisterInterface.ol/
type response {
message: string
}
interface RegisterInterface {
RequestResponse: register( void )( response )
}
register.ol/
from RegisterInterface import RegisterInterface
from time import Time
service Register {
execution: concurrent
embed Time as Time
inputPort Register {
location: "socket://localhost:2000"
protocol: sodep
interfaces: RegisterInterface
}
init
{
global.registered_users=0;
response.message = "Successful registration.\nYou are the user number "
}
main
{
register()( response ){
/* the synchronized section allows to access syncToken scope in mutual exclusion */
synchronized( syncToken ) {
response.message = response.message + ++global.registered_users;
sleep@Time( 2000 )()
}
}
}
}
The client executes five parallel calls to the service in order to register five different users.
main
{
{
register@Register()( response1 );
println@Console( response1.message )()
}
|
{
register@Register()( response2 );
println@Console( response2.message )()
}
|
{
register@Register()( response3 );
println@Console( response3.message )()
}
|
{
register@Register()( response4 );
println@Console( response4.message )()
}
|
{
register@Register()( response5 );
println@Console( response5.message )()
}
}
If executed, it is possible to observe that the parallel calls of the client are serialized by the service thanks to the primitive synchronized which implements a mutual access of the scope syncToken.