diff options
Diffstat (limited to 'doc/guide-tasks.md')
| -rw-r--r-- | doc/guide-tasks.md | 519 |
1 files changed, 0 insertions, 519 deletions
diff --git a/doc/guide-tasks.md b/doc/guide-tasks.md deleted file mode 100644 index c3bdbe3a3ee..00000000000 --- a/doc/guide-tasks.md +++ /dev/null @@ -1,519 +0,0 @@ -% The Rust Tasks and Communication Guide - -# Introduction - -Rust provides safe concurrency through a combination -of lightweight, memory-isolated tasks and message passing. -This guide will describe the concurrency model in Rust, how it -relates to the Rust type system, and introduce -the fundamental library abstractions for constructing concurrent programs. - -Rust tasks are not the same as traditional threads: rather, -they are considered _green threads_, lightweight units of execution that the Rust -runtime schedules cooperatively onto a small number of operating system threads. -On a multi-core system Rust tasks will be scheduled in parallel by default. -Because tasks are significantly -cheaper to create than traditional threads, Rust can create hundreds of -thousands of concurrent tasks on a typical 32-bit system. -In general, all Rust code executes inside a task, including the `main` function. - -In order to make efficient use of memory Rust tasks have dynamically sized stacks. -A task begins its life with a small -amount of stack space (currently in the low thousands of bytes, depending on -platform), and acquires more stack as needed. -Unlike in languages such as C, a Rust task cannot accidentally write to -memory beyond the end of the stack, causing crashes or worse. - -Tasks provide failure isolation and recovery. When a fatal error occurs in Rust -code as a result of an explicit call to `fail!()`, an assertion failure, or -another invalid operation, the runtime system destroys the entire -task. Unlike in languages such as Java and C++, there is no way to `catch` an -exception. Instead, tasks may monitor each other for failure. - -Tasks use Rust's type system to provide strong memory safety guarantees. In -particular, the type system guarantees that tasks cannot share mutable state -with each other. Tasks communicate with each other by transferring _owned_ -data through the global _exchange heap_. - -## A note about the libraries - -While Rust's type system provides the building blocks needed for safe -and efficient tasks, all of the task functionality itself is implemented -in the standard and extra libraries, which are still under development -and do not always present a consistent or complete interface. - -For your reference, these are the standard modules involved in Rust -concurrency at this writing: - -* [`std::task`] - All code relating to tasks and task scheduling, -* [`std::comm`] - The message passing interface, -* [`extra::comm`] - Additional messaging types based on `std::comm`, -* [`extra::sync`] - More exotic synchronization tools, including locks, -* [`extra::arc`] - The Arc (atomically reference counted) type, - for safely sharing immutable data, -* [`extra::future`] - A type representing values that may be computed concurrently and retrieved at a later time. - -[`std::task`]: std/task/index.html -[`std::comm`]: std/comm/index.html -[`extra::comm`]: extra/comm/index.html -[`extra::sync`]: extra/sync/index.html -[`extra::arc`]: extra/arc/index.html -[`extra::future`]: extra/future/index.html - -# Basics - -The programming interface for creating and managing tasks lives -in the `task` module of the `std` library, and is thus available to all -Rust code by default. At its simplest, creating a task is a matter of -calling the `spawn` function with a closure argument. `spawn` executes the -closure in the new task. - -~~~~ -# use std::task::spawn; - -// Print something profound in a different task using a named function -fn print_message() { println!("I am running in a different task!"); } -spawn(print_message); - -// Print something more profound in a different task using a lambda expression -spawn(proc() println!("I am also running in a different task!") ); -~~~~ - -In Rust, there is nothing special about creating tasks: a task is not a -concept that appears in the language semantics. Instead, Rust's type system -provides all the tools necessary to implement safe concurrency: particularly, -_owned types_. The language leaves the implementation details to the standard -library. - -The `spawn` function has a very simple type signature: `fn spawn(f: -proc())`. Because it accepts only owned closures, and owned closures -contain only owned data, `spawn` can safely move the entire closure -and all its associated state into an entirely different task for -execution. Like any closure, the function passed to `spawn` may capture -an environment that it carries across tasks. - -~~~ -# use std::task::spawn; -# fn generate_task_number() -> int { 0 } -// Generate some state locally -let child_task_number = generate_task_number(); - -spawn(proc() { - // Capture it in the remote task - println!("I am child number {}", child_task_number); -}); -~~~ - -## Communication - -Now that we have spawned a new task, it would be nice if we could -communicate with it. Recall that Rust does not have shared mutable -state, so one task may not manipulate variables owned by another task. -Instead we use *pipes*. - -A pipe is simply a pair of endpoints: one for sending messages and another for -receiving messages. Pipes are low-level communication building-blocks and so -come in a variety of forms, each one appropriate for a different use case. In -what follows, we cover the most commonly used varieties. - -The simplest way to create a pipe is to use `Chan::new` -function to create a `(Port, Chan)` pair. In Rust parlance, a *channel* -is a sending endpoint of a pipe, and a *port* is the receiving -endpoint. Consider the following example of calculating two results -concurrently: - -~~~~ -# use std::task::spawn; - -let (port, chan): (Port<int>, Chan<int>) = Chan::new(); - -spawn(proc() { - let result = some_expensive_computation(); - chan.send(result); -}); - -some_other_expensive_computation(); -let result = port.recv(); -# fn some_expensive_computation() -> int { 42 } -# fn some_other_expensive_computation() {} -~~~~ - -Let's examine this example in detail. First, the `let` statement creates a -stream for sending and receiving integers (the left-hand side of the `let`, -`(chan, port)`, is an example of a *destructuring let*: the pattern separates -a tuple into its component parts). - -~~~~ -let (port, chan): (Port<int>, Chan<int>) = Chan::new(); -~~~~ - -The child task will use the channel to send data to the parent task, -which will wait to receive the data on the port. The next statement -spawns the child task. - -~~~~ -# use std::task::spawn; -# fn some_expensive_computation() -> int { 42 } -# let (port, chan) = Chan::new(); -spawn(proc() { - let result = some_expensive_computation(); - chan.send(result); -}); -~~~~ - -Notice that the creation of the task closure transfers `chan` to the child -task implicitly: the closure captures `chan` in its environment. Both `Chan` -and `Port` are sendable types and may be captured into tasks or otherwise -transferred between them. In the example, the child task runs an expensive -computation, then sends the result over the captured channel. - -Finally, the parent continues with some other expensive -computation, then waits for the child's result to arrive on the -port: - -~~~~ -# fn some_other_expensive_computation() {} -# let (port, chan) = Chan::<int>::new(); -# chan.send(0); -some_other_expensive_computation(); -let result = port.recv(); -~~~~ - -The `Port` and `Chan` pair created by `Chan::new` enables efficient -communication between a single sender and a single receiver, but multiple -senders cannot use a single `Chan`, and multiple receivers cannot use a single -`Port`. What if our example needed to compute multiple results across a number -of tasks? The following program is ill-typed: - -~~~ {.ignore} -# use std::task::{spawn}; -# fn some_expensive_computation() -> int { 42 } -let (port, chan) = Chan::new(); - -spawn(proc() { - chan.send(some_expensive_computation()); -}); - -// ERROR! The previous spawn statement already owns the channel, -// so the compiler will not allow it to be captured again -spawn(proc() { - chan.send(some_expensive_computation()); -}); -~~~ - -Instead we can use a `SharedChan`, a type that allows a single -`Chan` to be shared by multiple senders. - -~~~ -# use std::task::spawn; - -let (port, chan) = SharedChan::new(); - -for init_val in range(0u, 3) { - // Create a new channel handle to distribute to the child task - let child_chan = chan.clone(); - spawn(proc() { - child_chan.send(some_expensive_computation(init_val)); - }); -} - -let result = port.recv() + port.recv() + port.recv(); -# fn some_expensive_computation(_i: uint) -> int { 42 } -~~~ - -Here we transfer ownership of the channel into a new `SharedChan` value. Like -`Chan`, `SharedChan` is a non-copyable, owned type (sometimes also referred to -as an *affine* or *linear* type). Unlike with `Chan`, though, the programmer -may duplicate a `SharedChan`, with the `clone()` method. A cloned -`SharedChan` produces a new handle to the same channel, allowing multiple -tasks to send data to a single port. Between `spawn`, `Chan` and -`SharedChan`, we have enough tools to implement many useful concurrency -patterns. - -Note that the above `SharedChan` example is somewhat contrived since -you could also simply use three `Chan` pairs, but it serves to -illustrate the point. For reference, written with multiple streams, it -might look like the example below. - -~~~ -# use std::task::spawn; -# use std::vec; - -// Create a vector of ports, one for each child task -let ports = vec::from_fn(3, |init_val| { - let (port, chan) = Chan::new(); - spawn(proc() { - chan.send(some_expensive_computation(init_val)); - }); - port -}); - -// Wait on each port, accumulating the results -let result = ports.iter().fold(0, |accum, port| accum + port.recv() ); -# fn some_expensive_computation(_i: uint) -> int { 42 } -~~~ - -## Backgrounding computations: Futures -With `extra::future`, rust has a mechanism for requesting a computation and getting the result -later. - -The basic example below illustrates this. - -~~~ -# fn make_a_sandwich() {}; -fn fib(n: u64) -> u64 { - // lengthy computation returning an uint - 12586269025 -} - -let mut delayed_fib = extra::future::Future::spawn(proc() fib(50)); -make_a_sandwich(); -println!("fib(50) = {:?}", delayed_fib.get()) -~~~ - -The call to `future::spawn` returns immediately a `future` object regardless of how long it -takes to run `fib(50)`. You can then make yourself a sandwich while the computation of `fib` is -running. The result of the execution of the method is obtained by calling `get` on the future. -This call will block until the value is available (*i.e.* the computation is complete). Note that -the future needs to be mutable so that it can save the result for next time `get` is called. - -Here is another example showing how futures allow you to background computations. The workload will -be distributed on the available cores. - -~~~ -# use std::vec; -fn partial_sum(start: uint) -> f64 { - let mut local_sum = 0f64; - for num in range(start*100000, (start+1)*100000) { - local_sum += (num as f64 + 1.0).powf(&-2.0); - } - local_sum -} - -fn main() { - let mut futures = vec::from_fn(1000, |ind| extra::future::Future::spawn( proc() { partial_sum(ind) })); - - let mut final_res = 0f64; - for ft in futures.mut_iter() { - final_res += ft.get(); - } - println!("π^2/6 is not far from : {}", final_res); -} -~~~ - -## Sharing immutable data without copy: Arc - -To share immutable data between tasks, a first approach would be to only use pipes as we have seen -previously. A copy of the data to share would then be made for each task. In some cases, this would -add up to a significant amount of wasted memory and would require copying the same data more than -necessary. - -To tackle this issue, one can use an Atomically Reference Counted wrapper (`Arc`) as implemented in -the `extra` library of Rust. With an Arc, the data will no longer be copied for each task. The Arc -acts as a reference to the shared data and only this reference is shared and cloned. - -Here is a small example showing how to use Arcs. We wish to run concurrently several computations on -a single large vector of floats. Each task needs the full vector to perform its duty. - -~~~ -# use std::vec; -# use std::rand; -use extra::arc::Arc; - -fn pnorm(nums: &~[f64], p: uint) -> f64 { - nums.iter().fold(0.0, |a,b| a+(*b).powf(&(p as f64)) ).powf(&(1.0 / (p as f64))) -} - -fn main() { - let numbers = vec::from_fn(1000000, |_| rand::random::<f64>()); - println!("Inf-norm = {}", *numbers.iter().max().unwrap()); - - let numbers_arc = Arc::new(numbers); - - for num in range(1u, 10) { - let (port, chan) = Chan::new(); - chan.send(numbers_arc.clone()); - - spawn(proc() { - let local_arc : Arc<~[f64]> = port.recv(); - let task_numbers = local_arc.get(); - println!("{}-norm = {}", num, pnorm(task_numbers, num)); - }); - } -} -~~~ - -The function `pnorm` performs a simple computation on the vector (it computes the sum of its items -at the power given as argument and takes the inverse power of this value). The Arc on the vector is -created by the line - -~~~ -# use extra::arc::Arc; -# use std::vec; -# use std::rand; -# let numbers = vec::from_fn(1000000, |_| rand::random::<f64>()); -let numbers_arc=Arc::new(numbers); -~~~ - -and a clone of it is sent to each task - -~~~ -# use extra::arc::Arc; -# use std::vec; -# use std::rand; -# let numbers=vec::from_fn(1000000, |_| rand::random::<f64>()); -# let numbers_arc = Arc::new(numbers); -# let (port, chan) = Chan::new(); -chan.send(numbers_arc.clone()); -~~~ - -copying only the wrapper and not its contents. - -Each task recovers the underlying data by - -~~~ -# use extra::arc::Arc; -# use std::vec; -# use std::rand; -# let numbers=vec::from_fn(1000000, |_| rand::random::<f64>()); -# let numbers_arc=Arc::new(numbers); -# let (port, chan) = Chan::new(); -# chan.send(numbers_arc.clone()); -# let local_arc : Arc<~[f64]> = port.recv(); -let task_numbers = local_arc.get(); -~~~ - -and can use it as if it were local. - -The `arc` module also implements Arcs around mutable data that are not covered here. - -# Handling task failure - -Rust has a built-in mechanism for raising exceptions. The `fail!()` macro -(which can also be written with an error string as an argument: `fail!( -~reason)`) and the `assert!` construct (which effectively calls `fail!()` -if a boolean expression is false) are both ways to raise exceptions. When a -task raises an exception the task unwinds its stack---running destructors and -freeing memory along the way---and then exits. Unlike exceptions in C++, -exceptions in Rust are unrecoverable within a single task: once a task fails, -there is no way to "catch" the exception. - -While it isn't possible for a task to recover from failure, tasks may notify -each other of failure. The simplest way of handling task failure is with the -`try` function, which is similar to `spawn`, but immediately blocks waiting -for the child task to finish. `try` returns a value of type `Result<T, -()>`. `Result` is an `enum` type with two variants: `Ok` and `Err`. In this -case, because the type arguments to `Result` are `int` and `()`, callers can -pattern-match on a result to check whether it's an `Ok` result with an `int` -field (representing a successful result) or an `Err` result (representing -termination with an error). - -~~~{.ignore .linked-failure} -# use std::task; -# fn some_condition() -> bool { false } -# fn calculate_result() -> int { 0 } -let result: Result<int, ()> = task::try(proc() { - if some_condition() { - calculate_result() - } else { - fail!("oops!"); - } -}); -assert!(result.is_err()); -~~~ - -Unlike `spawn`, the function spawned using `try` may return a value, -which `try` will dutifully propagate back to the caller in a [`Result`] -enum. If the child task terminates successfully, `try` will -return an `Ok` result; if the child task fails, `try` will return -an `Error` result. - -[`Result`]: std/result/index.html - -> ***Note:*** A failed task does not currently produce a useful error -> value (`try` always returns `Err(())`). In the -> future, it may be possible for tasks to intercept the value passed to -> `fail!()`. - -TODO: Need discussion of `future_result` in order to make failure -modes useful. - -But not all failures are created equal. In some cases you might need to -abort the entire program (perhaps you're writing an assert which, if -it trips, indicates an unrecoverable logic error); in other cases you -might want to contain the failure at a certain boundary (perhaps a -small piece of input from the outside world, which you happen to be -processing in parallel, is malformed and its processing task can't -proceed). - -## 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 `extra::comm::DuplexStream()` supports this pattern. We'll -look briefly at how to use it. - -To see how `DuplexStream()` works, we will create a child task -that repeatedly receives a `uint` message, converts it to a string, and sends -the string in response. The child terminates when it receives `0`. -Here is the function that implements the child task: - -~~~{.ignore .linked-failure} -# use extra::comm::DuplexStream; -# use std::uint; -fn stringifier(channel: &DuplexStream<~str, uint>) { - let mut value: uint; - loop { - value = channel.recv(); - channel.send(uint::to_str(value)); - if value == 0 { break; } - } -} -~~~~ - -The implementation of `DuplexStream` supports both sending and -receiving. The `stringifier` function takes a `DuplexStream` that can -send strings (the first type parameter) and receive `uint` messages -(the second type parameter). The body itself simply loops, reading -from the channel and then sending its response back. The actual -response itself is simply the stringified version of the received value, -`uint::to_str(value)`. - -Here is the code for the parent task: - -~~~{.ignore .linked-failure} -# use std::task::spawn; -# use std::uint; -# use extra::comm::DuplexStream; -# fn stringifier(channel: &DuplexStream<~str, uint>) { -# let mut value: uint; -# loop { -# value = channel.recv(); -# channel.send(uint::to_str(value)); -# if value == 0u { break; } -# } -# } -# fn main() { - -let (from_child, to_child) = DuplexStream::new(); - -spawn(proc() { - stringifier(&to_child); -}); - -from_child.send(22); -assert!(from_child.recv() == ~"22"); - -from_child.send(23); -from_child.send(0); - -assert!(from_child.recv() == ~"23"); -assert!(from_child.recv() == ~"0"); - -# } -~~~~ - -The parent task first calls `DuplexStream` to create a pair of bidirectional -endpoints. It then uses `task::spawn` to create the child task, which captures -one end of the communication channel. As a result, both parent and child can -send and receive data to and from the other. |
