Edit some things, make sure all code runs.
6.1 KiB
Tasks
Rust supports a system of lightweight tasks, similar to what is found in Erlang or other actor systems. Rust tasks communicate via messages and do not share data. However, it is possible to send data without copying it by making use of unique boxes, which allow the sending task to release ownership of a value, so that the receiving task can keep on using it.
NOTE: As Rust evolves, we expect the Task API to grow and change somewhat. The tutorial documents the API as it exists today.
Spawning a task
Spawning a task is done using the various spawn functions in the
module task
. Let's begin with the simplest one, task::spawn()
:
let some_value = 22;
let child_task = task::spawn {||
std::io::println("This executes in the child task.");
std::io::println(#fmt("%d", some_value));
};
The argument to task::spawn()
is a unique
closure of type fn~()
, meaning that it takes no
arguments and generates no return value. The effect of task::spawn()
is to fire up a child task that will execute the closure in parallel
with the creator. The result is a task id, here stored into the
variable child_task
.
Ports and channels
Now that we have spawned a child task, it would be nice if we could communicate with it. This is done by creating a port with an associated channel. A port is simply a location to receive messages of a particular type. A channel is used to send messages to a port. For example, imagine we wish to perform two expensive computations in parallel. We might write something like:
# fn some_expensive_computation() -> int { 42 }
# fn some_other_expensive_computation() {}
let port = comm::port::<int>();
let chan = comm::chan::<int>(port);
let child_task = task::spawn {||
let result = some_expensive_computation();
comm::send(chan, result);
};
some_other_expensive_computation();
let result = comm::recv(port);
Let's walk through this code line-by-line. The first line creates a port for receiving integers:
let port = comm::port::<int>();
This port is where we will receive the message from the child task
once it is complete. The second line creates a channel for sending
integers to the port port
:
# let port = comm::port::<int>();
let chan = comm::chan::<int>(port);
The channel will be used by the child to send a message to the port. The next statement actually spawns the child:
# fn some_expensive_computation() -> int { 42 }
# let port = comm::port::<int>();
# let chan = comm::chan::<int>(port);
let child_task = task::spawn {||
let result = some_expensive_computation();
comm::send(chan, result);
};
This child will perform the expensive computation send the result over the channel. Finally, the parent continues by performing some other expensive computation and then waiting for the child's result to arrive on the port:
# fn some_other_expensive_computation() {}
# let port = comm::port::<int>();
some_other_expensive_computation();
let result = comm::recv(port);
Creating a task with a bi-directional communication path
A very common thing to do is to spawn a child task where the parent
and child both need to exchange messages with each other. The function
task::spawn_connected()
supports this pattern. We'll look briefly at
how it is used.
To see how spawn_connected()
works, we will create a child task
which receives uint
messages, converts them to a string, and sends
the string in response. The child terminates when 0
is received.
Here is the function which implements the child task:
fn stringifier(from_par: comm::port<uint>,
to_par: comm::chan<str>) {
let value: uint;
do {
value = comm::recv(from_par);
comm::send(to_par, uint::to_str(value, 10u));
} while value != 0u;
}
You can see that the function takes two parameters. The first is a
port used to receive messages from the parent, and the second is a
channel used to send messages to the parent. The body itself simply
loops, reading from the from_par
port and then sending its response
to the to_par
channel. The actual response itself is simply the
strified version of the received value, uint::to_str(value)
.
Here is the code for the parent task:
# fn stringifier(from_par: comm::port<uint>,
# to_par: comm::chan<str>) {}
fn main() {
let t = task::spawn_connected(stringifier);
comm::send(t.to_child, 22u);
assert comm::recv(t.from_child) == "22";
comm::send(t.to_child, 23u);
assert comm::recv(t.from_child) == "23";
comm::send(t.to_child, 0u);
assert comm::recv(t.from_child) == "0";
}
The call to spawn_connected()
on the first line will instantiate the
various ports and channels and startup the child task. The returned
value, t
, is a record of type task::connected_task<uint,str>
. In
addition to the task id of the child, this record defines two fields,
from_child
and to_child
, which contain the port and channel
respectively for communicating with the child. Those fields are used
here to send and receive three messages from the child task.
Joining a task
The function spawn_joinable()
is used to spawn a task that can later
be joined. This is implemented by having the child task send a message
when it has completed (either successfully or by failing). Therefore,
spawn_joinable()
returns a structure containing both the task ID and
the port where this message will be sent---this structure type is
called task::joinable_task
. The structure can be passed to
task::join()
, which simply blocks on the port, waiting to receive
the message from the child task.
The supervisor relationship
By default, failures in Rust propagate upward through the task tree.
We say that each task is supervised by its parent, meaning that if the
task fails, that failure is propagated to the parent task, which will
fail sometime later. This propagation can be disabled by using the
function task::unsupervise()
, which disables error propagation from
the current task to its parent.