A task on a carrier shares everything with the program: the same memory, the same descriptors, the same fate. That is what makes it cheap, and it is what data-race freedom guards. Some work is better done apart: a parser fed input you do not trust, a computation that may trap, a library you would rather not let take your server down with it. For that work, one word changes where a task runs:
schedule Parse(input) on process;on process places the task in a worker process of the same program. The
block around it is the block the tasks page walked: it starts
the task, joins it at its end, and hands its failure to an on fail section.
The task is the same task. What changes is what it shares, which is nothing.
The example is examples/ProcessTasks
in the tutorial repository.
Getting the file
make -C mica-container/examples/ProcessTasks runProcessTasks
One word apart
the task ran in the program's own process
the task ran in a process of its own
What crosses is a copy
the worker's copy: Ada, 4 items, total 100
the program's copy: Ada, 3 items
A worker that traps ends alone
quotient 5
Mica runtime failure: reason=division_by_zero (1), token_stream_index=232
Mica runtime context: file=ProcessTasks.mica, line=74, column=34
Mica runtime source: WriteLn(" quotient %lld", a / b);
the dividing worker was lost
the program goes on
A connection handed over
the client read: served by a worker process
A task from another unit
checksum of 'the quick brown fox': 18561The three lines that begin with Mica runtime are a worker’s failure report,
written to standard error; everything else is standard output.
The example, walked
One word apart
concurrent
schedule Where(ProcessId());
end;
concurrent
schedule Where(ProcessId()) on process;
on lost do
WriteLn(" the worker was lost")
end;Where compares the process it runs in with the program’s, which it is handed.
The first placement runs on a carrier, inside the program’s own process; the
second runs in a worker. A worker is a fork of a small server the program
starts before its first statement, so a placement costs a fork, not the start of
a second program.
The one addition is the on lost section. A worker can end before its task
does: a trap, a kill, the machine running out of memory. That is no failure of
the task’s own domain, so it has a section of its own, and the compiler holds
you to it: a block that places a task on a process without one is refused
(5586), and a block that places none is refused one (5587).
What crosses is a copy
task Complete(o : Order);
...
mine := o;
Append(address mine.items, 40); the worker's copy: Ada, 4 items, total 100
the program's copy: Ada, 3 itemsA task’s arguments are snapshots on a carrier too, but there the snapshot is a rule the compiler keeps. Here it is the only thing that can happen: the worker holds none of the program’s memory, so every argument travels into it, text, arrays and records holding both, and whatever the task does to its copy stays in the worker. For the same reason a task on a process reaches no state outside its own frame. A program variable it wrote would change only in the worker’s copy, so the compiler refuses the attempt and says what to do instead (5583).
A task returns nothing, here as on a carrier. What a worker produces comes back
through its output, as in this example, through its failure code in the block’s
on fail section, through a stream a worker feeds,
or through a connection, which is the next section.
A worker that traps ends alone
concurrent
schedule Divide(10, 0) on process;
on lost do
WriteLn(" the dividing worker was lost")
end;
WriteLn(" the program goes on");The division by zero traps in the worker. The worker writes the runtime failure
report the program would have written, naming the line, and ends. The block’s
on lost section runs after the join, and the program goes on. A lost worker
cancels the block’s other tasks, as a failure does. On a carrier the same trap
ends the program; the first exercise below shows it.
A connection handed over
served := Accept(listener) on fail problem do WriteLn(" accept failed");
concurrent
schedule Serve(served) on process;
Close(address served);
on lost do
WriteLn(" the serving worker was lost")
end;A Connection holds a descriptor, the kernel’s name for an open socket. Its
number would mean nothing in the worker, where it names whatever the worker
holds under it, so the worker receives the descriptor itself: the placement
duplicates it, and the request hands the duplicate to the worker beside its
bytes. The worker’s descriptor names the same socket, so the program closes its
own the moment the placement returns, and the connection stays open until the
worker is done with it. That is how a server hands each connection to a worker
process: Accept in a loop, schedule ... on process, Close. A Listener
crosses the same way, so workers can accept on the program’s listening socket.
A closed connection’s descriptor names nothing, and it crosses closed. A number that names nothing open in the program at the placement, such as a copy of a connection whose original was closed, is refused there by name. The worker never receives whatever the number happens to name by then.
A task from another unit
imp
Checksum : checksums;
...
schedule Checksum("the quick brown fox") on process;The object unit checksums exports a task, and the program places it on a
process exactly as it places its own. A library’s exported task is placed the
same way, and a library may place its own tasks on processes: a program that
links such a library starts the worker server because the library’s contract
says so.
How many workers run at once
At most so many workers run at once, by default one for each processor the
program may use when it starts. A task placed beyond the count waits, holding
no carrier, until a worker ends. --processes N sets the count when the program
is built, and +mica processes=N -mica when it starts, where the build’s policy
admits runtime flags. The runtime flags
reference has the whole rule.
One consequence is worth knowing before you meet it. Tasks on processes that wait for each other, such as a server and its client placed side by side, need a count at least as large as the tasks that wait together: a task still waiting for a worker cannot answer the one that already has one. A stream a worker feeds takes no place, because its loop paces it.
What the compiler refused
| The program tried to | The answer |
|---|---|
place a task on a process in a block without on lost | 5586: 'Divide' is placed on a process, whose worker can end before the task finishes - a trap, a kill, the machine running out of memory: add an 'on lost do' section … |
keep an on lost section in a block that places no task on a process | 5587: this concurrent block places no task on a process, so no worker can be lost: remove the 'on lost' section |
| change a program variable from a task on a process | 5583: 'Divide' is placed on a process, and its work reaches state outside its own frame, which the worker would change only in its own copy: pass what 'Divide' needs as arguments |
What this does not do
- No shared memory. A task on a process sees copies of its arguments and nothing else. Work that must share state runs on carriers, under data-race freedom.
- One task per worker. Each placement forks a fresh worker, which ends with its task. Nothing carries over from one placement to the next.
- One machine. Workers are processes of the machine the program runs on. Processes that are not one program’s workers talk over the IPC road or a TCP connection.
Try it
1. Trap on a carrier. Take on process away from the Divide(10, 0)
placement. The compiler now refuses the block’s on lost section (5587),
because no worker can be lost. Remove the section too, and run. The first two
sections print as before, and then:
A worker that traps ends alone
quotient 5
Mica runtime failure: reason=division_by_zero (1), token_stream_index=232
Mica runtime context: file=ProcessTasks.mica, line=74, column=34
Mica runtime source: WriteLn(" quotient %lld", a / b);The report is the same. What it ends is not: on a carrier the trap ends the program, which exits with code 1, and nothing after it prints.
2. Close too early. Move Close(address served) above the concurrent
block, and run:
A connection handed over
the worker could not answerA closed connection crosses closed, so the worker’s write fails with Ebadf
and the client reads nothing. The order in the example is the one that works:
place, then close.
3. Reach for the program’s order. In Divide, write
synchronized order.customer := "Bea"; before the WriteLn. On a carrier that
is a synchronized write to shared state. On a process the compiler refuses it
(5583), because the worker would change only its own copy.
Next
A producer can live in a worker too: Channel streams places one on a process the same way, and its elements cross back as frames. When the processes are separate programs rather than one program’s workers, the IPC road connects them.