Sage-Code Laboratory
index<--

Eve Cocurency

Eve can execute one or more routines in synchronous or asynchronous mode. We will define in this chapter the routine and the coroutine. A routine can be suspended in memory, and later resumed. Using this system we can create light weight multi-treding applications.

Routines

Before we can create coroutines we must understand the concept of simple routine. In Eve we create a subprogram that can receive parameters and can have side-effects. You can call a routine by name, with or without arguments enumerated in a list. A routine is created with keyword "routine" and is ending with keyword "return". Routine do not have to be follow by empty paranthesis when it has no arguments.

Syntax:


  routine name(parameter_list):
     ** routine statements
     ...
  return;

Example

Next example demonstrate two routines: "foo" and "bar". We declare the routines and then call the routines. One of routine has no parameters, the other has 3 parameters. Two input and one output parameter.


# demonstrate two routines
driver process_demo:

** foo has no parameters
routine foo:
  print "I have no arguments";
return;

** bar has 3 parameters
routine bar(param1:Integer, 
            param2 = 0: Integer,
            @result = 0: Integer):
  print "I have two parameters: param1 and param2";
  let result = param1 + param2;                    
return;

process:
  foo; -- call routine without arguments
  new output:=0; -- prepare a variable to receive output

  ** execute routine with 2 arguments and receiver
  bar(1,2,output);
  expect output == 3; -- the value of output should be 3.
  print ("outout=", output);
return;

Side Effects

A process can use process states and can have side-effects. When a global state is changed by a routine or method this is called side-effect. This may be useful but potential unsafe. Some side effects may be harder to debug. It is safer to use input/output parametters than side effects.

these are side effects...

Examples

Next routine: "add_numbers" has side effects:


driver side_effect:
  set test   :Integer;
  set p1, p2 :Integer;

** routine with side effects
routine add_numbers:
  let test := p1 + p2; -- first side-effect
  print test;          -- second side-effect
return;

** define main process
process:
  let p1 := 10;
  let p2 := 20;
  add_numbers; --  routine call without arguments
  expect test == 30;
return;

Note: In Eve it is not required to use empty paranthesis() to call a routine. Only functions require empty brackets. We will talk about functions later. This feature enable us to extend Eve using Eve scripts.

Parameters

Parameters  are defined in round brackets () separated by comma. Each parameter must have type and name. Parameters are optional. Eve offer a range of advanced features: optional arguments, default values, variable number of arguments and input/output arguments.

Parameter Syntax:

Mandatory parameters do not have initial values but only declared type. Optional parameters have initial value that is assign using operator "=" with explicit :type, or ":=" with type inference. Arguments can be pass by position or by name.

Notes:

  1. One routine can receive one or more parameters,
  2. Parameters having initial values are optional,
  3. Values used for parameters at runtime, are called "arguments",
  4. You can assign arguments by position using a list of values,
  5. You can assign arguments by name using (name:value) pairs;

Vararg parameters

One routine can receive multiple arguments of the same type into a single collection parameter. This can be a List, DataSet or HashMap, depending on declaration.

Note: Also driver and aspect can receive parameters. The same convention apply for aspects, drivers, functions and methods. Eve offer same features for all subprograms. The only difference are the call concentions.

Example:

Next driver can have a list of arguments. The arguments have no names, so it will just print all the arguments it has received. Drivers receive argument values from command line or REPL console using "start" command.

# print all arguments
driver test(*args: ()String):

** list all arguments
process:
  cycle:
    new arg :String;
  for arg in args loop
    print arg;  -- expect 1,2,3
  repeat;
return;

eve:>start test.eve 1 2 3

Output Parameters

To avoid overuse of global variables you must use input/output parameters. We prefix output parameters using symbol "@". Output parameters require a variable as argument, otherwise you will not be able to capture the output value. Compiler will complain if you do not send a valid refference to an output parameter.


#demo output parameters
aspect output_params:

** private subroutine
routine add(p1 = 0, p2 = 1: Integer, @op: Integer):
  let op := p1 + p2;
return;

process:
  new result: Integer;

  ** inpur/output argument require a variable
  add(1,2, op:result);
  print result;  -- expected value 3

  ** negative test, will fail
  add(1,2,4); -- error, "op" parameter require a variable
return;

Notes:

Coroutines

A coroutine is a special routine that has a name and can start a secondary thread. A coroutine can be suspended in memory for a while and later can wake-up with a signal and continue processing. The main thread will conntrol the coroutine life cycle.

A coroutine is created using same keyword "routine" but the call convention is different. Unlike a simple routine you must use keyword "start" to initialize a coroutine. This keyword suggest that the routine start running but is not fully executed and will terminate later. The coroutine is controlled using "suspend", "resume" and "wait/all" keywords.

Asynchronous Execution

Synchronous vs Asynchronous

Shoulder Thread

In next example we demonstrate a thread that can generate a a bunch of numbers every time the main thred is yielding for it. When the buffer is full, you can routine the batch and then yield for a new.


driver shoulder_thread:

** coroutine producer, make 100 numbers
routine generator(cap:Integer, @result:()Float):
  new count := 0;
  loop
    for i in (1..100) loop
      let result += (0..1).random();
    repeat;
    suspend; -- continue the main thread    
    let count += 1;
  repeat if count < cap;
return;

routine:
  new batch :()Float;
  new total :Real;
  ** initialize the generator and produce first batch
  start generator(1000, result:batch):
  while batch.count() > 0 loop
    ** routine current batch
    for x in batch loop
       let total += x; 
    repeat;
    ** read the next batch
    resume generator; -- resume generator
  repeat;
return;

Time-out: Is easy to create a wrong shoulder thread that can runs forever. If a routine takes too much time you can set a time-out. If one routine time-out, the entire application crash. You can set $timeout system variable to control this time.

Multi Threading

This design pattern enable you do create two coroutines that wait for each other. It is common practice to use this pattern to create a multi-thread applications. The threads communicate using one or more channels.

Example


    routine producer(@pipeline: ()Integer, count = 1000: Integer):
    for i in (1..count) loop
      let pipeline += random(100);
      if (i % 100) == 0 then
        resume consumer;
        suspend;
      done;
    repeat;
  return;
  
  routine consumer(@pipeline, @partials: ()Integer):
    loop
      for element in pipeline loop
        let pipeline := pipeline -> element;
        wait 10ms;
        let partials += element; 
      repeat;
      resume producer;
      suspend;
    repeat if pipeline.count() > 0;
  return;
  
  routine:
    new pipeline: ()Integer;
    new partials: ()Integer; 
  
    start producer(pipeline, 10000);
    for i in (1..32) loop
      start consumer(pipeline, partials);
      wait 3ms;
    repeat;
  
    wait all;  -- Wait for all routines to finish
    
    ** prepare the final result from partials
    new result := sum(partials);
    print result;
  return;

Notes: a suspended routine is waiting for a wake-up signal. Another routine must send a signal for it to wake-up. If the coroutine receiving the signal and is not running, then it can't wake-up. In this case, the control is given back to the main process. If the main process is waiting it will resume.

We can see in this example that "wait all" and "wait n" do different things. "wait n" should not be used for joining threads. We recommend to use "wait all". Also you can wait for a specific tread to finish: wait [routine_name] if necesary.

Control over number of threads can be dynamic. You can use configuration files and establish convention to calculate the number of threads depending on data size. It can be also hard coded using constants inside the driver. This give you flexibility to tune your process.


Read next: Functions