How it works...

A fundamental building block of parallelism in Rust is the Arc, which stands for Atomically Reference Counted. Functionally, it works the same way as an Rc, which we have looked at in Chapter 5, Advanced Data Structures; Sharing ownership with smart pointers. The only difference is that the reference counting is done using atomic primitives, which are versions of primitive data types like usize that have well-defined parallel interactions. This has two consequences:

  • An Arc is slightly slower than an Rc, as the reference counting involves a bit more work
  • An Arc can be used safely across threads

The constructor of Arc looks the same as Rc[7]:

let some_resource = Arc::new("Hello World".to_string());

This creates an Arc over a String. A String is a struct that is not inherently saved to be manipulated across threads. In Rust terms, we say that String is not Sync (more about that later in the recipe Atomically access primitives).

Now let's look at how a thread is initialized. thread::spawn() takes a closure and executes it in a new thread. Because this is done in parallel, the main thread doesn't wait until the thread is done; it continues working right after its creation.

The following creates a thread that prints out the content of some_resource and gives us a handle to that thread called thread_a[10]:

    let thread_a = {
let some_resource = some_resource.clone();
thread::spawn(move || {
println!("Thread A says: {}", some_resource);
})
};

Afterward (or at the same time), we do the exact same thing in a second thread called thread_b.

To understand why we need an Arc and can't just pass the resource directly to the closure, let's take a closer look at how closures work.

Closures in Rust can only operate on three kinds of variables:

  • Arguments passed to them
  • static variables (variables with the 'static lifetime; see Chapter 5, Advanced Data Structures; Creating lazy static objects)
  • Variables it owns, either by creating them or by moving them into the closure

With this in mind, let's look at the most simplistic approach an inexperienced Rust programmer might take:

let thread_a = thread::spawn(|| {
println!("Thread A says: {}", some_resource);
});

If we try to run this, the compiler tells us the following:

Seems like it doesn't like our usage of some_resource. Look at the rules for variable usage in closures again:

  • some_resource has not been passed as an argument
  • It is not static
  • It was neither created in the closure nor moved into it

But what does closure may outlive the current function mean? Well, because closures can be stored in a normal variable, they can be returned from a function. Imagine now if we programmed a function that created a variable called some_resource, used it inside a closure, and returned it. Since the function owns some_resource, it would be dropped while returning the closure, making any reference to it invalid. We don't want any invalid variables, so the compiler stops us from potentially enabling them. Instead, it suggests moving the ownership of some_resource into the closure by using the move keyword. Let's try that:

    let thread_a = thread::spawn(move || {
println!("Thread A says: {}", some_resource);
});

The compiler responds with this:

Because we moved some_resource into the closure inside of thread_a, thread_b can no longer use it! The solution is to create a clone of the reference to some_resource and only move the clone into the closure:

    let some_resource_clone = some_resource.clone();
let thread_a = thread::spawn(move || {
println!("Thread A says: {}", some_resource_clone);
});

This now runs perfectly fine, but it looks a bit weird, as we are now carrying the mental baggage of the knowledge that the resource we're dealing with is, in fact, a clone. This can be solved in a more elegant way by putting the clone into a new scope, where it can have the same name as the original, leaving us with the final version of our code:

let thread_a = {
let some_resource = some_resource.clone();
thread::spawn(move || {
println!("Thread A says: {}", some_resource);
})
};

Looks way clearer, doesn't it? This way of passing Rc and Arc variables to a closure is a well-known Rust idiom that we are going to use in all other recipes of the chapter from here on out.

The last thing we are going to do in this recipe is join the two threads by calling .join() on them [26 and 27]. Joining a thread means blocking the current thread until the joined thread is done with its work. It's called like that because we join the two threads of our program back into a single one. It helps to visually imagine actual sewing threads when thinking about this concept.

We join them before the end of the program, as otherwise, we would have no guarantee that they would actually run all the way through before our program quits. Generally speaking, you should join your threads when you need their results and can't wait for them any longer, or they're about to be dropped otherwise.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.218.221