-
Notifications
You must be signed in to change notification settings - Fork 26
RFC: 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.
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
\Gamma |- expr : t
----------------------
\Gamma |- async expr : Fut t
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
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:
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
\Gamma |- expr : t
-----------------------
\Gamma |- finish expr : void
An attached task is one that needs to be run by its active object / hot object:
- 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).
- 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:
- 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).
- If any argument is thread and/or subordinate, the task is in attached mode.