Skip to content
Kiko edited this page Oct 12, 2016 · 2 revisions

Tasks

Tasks are means to express parallel computations at a much higher fine grain than using actors. When a task is spawned, a future is returned and the common operations for futures can be applied on them (namely get, await and ~~>). Apart from these operations, we introduce a new keyword finish which guarantees that all futures created in its body will be finished before we can execute the next expression or statement following finish. More details and examples and provided in the next sections.

Async construct

Syntax

Tasks can be spawn with the keyword async (example based on RFC Local Functions)

def globalFunction(x: uint): Fut double
  async piDecimals(x)
where
  fun piDecimals(x: uint) : double
    functionBody
  end
end

in this case and based on RFC: Local Functions and Closures, the local function cannot capture this nor state from the function body and these need to be passed to the function.

In terms of syntactic sugar, we could desugar the following example:

def globalFunction(x: uint, y: int, z: int): Fut double
  let fut = async { 
              -- do something with x y and z (capture their state)
              functionBody that uses x y and z
            }
  in fut
end

into this (more details regarding partial function application RFC: Local Functions and Closures):

def globalFunction(x: uint, y: int, z: int): Fut double
  let closure = fun (x: uint, y: int, z: int) = { functionBody from above } 
      partialFnApplication = f(x, y, z) 
      fut = async partialFnApplication()
  in fut
end

Type checking rule

\Gamma |- expr : t
----------------------
\Gamma |- async expr : Fut t

Finish construct

Syntax

Futures spawned by actors and/or tasks are guaranteed to be fulfilled by the end of finish statement. For instance:

class Actor
  def foo(): int
    ...

def globalFunction(x: int): int
  let actorX = new Actor(x)
      actorY = new Actor(x+1)
  in
    finish {
      let futX = actorX.foo();
      let futY = actorY.foo(x);
      -- do something  
    }
    -- at this point, we do know that the futX and futY are finished
    -- if we need guarantees that the actors should be in some state,
    -- by this point, you can assume that

Computational model

finish statement follows the strict computational model, same as X10. A strict computation model allows a task to live after the parent has finished. For instance, in the following image:

alt text

The left node is the cilk fully strict computational model, where each spawn task is merged with its parent (the ''spawner''); the right node is the X10 strict computational model where each spawn task can be merged by one of its ancestors (using the finish construct). For instance:

def globalFun1(seed: int): int
  new Random(seed).next()

def globalFun2(seed: int): int
  async globalFun1(seed);

def mainFunction(): void
  finish {
    let futFun1 = async globalFun1(1)
        futFun2 = async globalFun2(1)
    in 

Type checking rule

\Gamma |- expr : t
-----------------------
\Gamma |- finish expr : void

Attached vs Detached task: it's all in the types!

An attached task is one that needs to be run by its active object / hot object:

  1. If the task has been spawned by an active object, the task runs in the same logical thread than the active object (not in parallel).
  2. If the task has been spawned by a hot object, the task may run in parallel to other methods processed by the hot object.

A detached task is one that can be run by any logical thread, actor, task runner, etc and the scheduling of such tasks are left as an implementation detail.

Optimisations:

Tasks may receive linear, thread, read and/or subordinate variables as arguments to their functions:

  1. If all arguments are linear and/or read, the task is in detached mode (and can be run by any one, as there is no possibility of data races).
  2. If any argument is thread and/or subordinate, the task is in attached mode.