rust/doc/tutorial/task.md
Marijn Haverbeke 0f72c53fdf Go over the tutorial again
Edit some things, make sure all code runs.
2012-01-12 13:19:02 +01:00

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.