Threads

Afinal, o que são Threads? Podemos considerar threads como sub processos que compartilham da mesma memória, diferente de estratégias como "fork" que é feita uma cópia do processo com espeço de memória isolado, as threads são sempre do mesmo processo e custam menos para serem criadas.

Estas threads são executadas de forma assíncrona, ou seja, todas de uma vez, claro existem estratégias para a execução dessa thread que o seu sistema operacional provavelmente usa.

Threads em Rust

Quando um processo é criado temos a thread principal, que parte a partir da função main, a partir desta threads conseguimos criar threads novas.

use std::thread;

fn main() {
    thread::spawn(|| {
        println!("Hello World!");
    });
}

Notem que utilizamos uma closure para dizer qual o comportamento que esta thread terá. Ao executarmos este programa temos o segu... espera, não temos output, como assim? A thread não executou? Sim, ela executou, porém, não teve tempo o suficiente para escrever a mensagem na saída, já que a criação de uma thread não bloqueia a thread que a criou, ela continua sendo executada, o que podemos fazer para bloquear a thread que criou a thread é chamar o método 'join', do valor retornado por esse método thread::spawn, ele nos retorna um JoinHandle<T>.

use std::thread::{self, JoinHandle};

fn main() {
    let handle: JoinHandle<()> = thread::spawn(|| {
        println!("Hello World!");
    });
    handle.join().unwrap();
}

Agora executando o código acima, criamos uma segunda thread, e bloqueamos a thread principal, quando a thread criada termina sua execução a thread principal é liberada. Também é possível recuperar um valor de dentro da thread, basta a closure retornar um valor.

use std::thread::{self, JoinHandle};

fn main() {
    let handle: JoinHandle<usize> = thread::spawn(|| 42);
    let valor = handle.join().unwrap();
    println!("valor da retornado pela thread = {}", valor);
}

Vamos fazer um teste com mais threads agora, cada uma imprimindo um valor de 0 a 10 e bloqueando a thread principal até o termino dessa execução.

use std::thread;

fn main() {
    let handle1 = thread::spawn(|| {
        for i in 0..10 {
            println!("da thread 1: {i}");
        }
    });
    let handle2 = thread::spawn(|| {
        for i in 0..10 {
            println!("da thread 2: {i}");
        }
    });
    let handle3 = thread::spawn(|| {
        for i in 0..10 {
            println!("da thread 3: {i}");
        }
    });
    handle1.join().unwrap();
    handle2.join().unwrap();
    handle3.join().unwrap();
}

Note que cada execução vai gerar uma saída diferente, pode ser que a thread 1 execute primeiro, e logo em sequência a 3, pode ser que elas se misturem, isso acontece justamente por serem executadas de forma assíncrona.

Concorrência e paralelismo

Concorrência e paralelismo são temas diferentes que andam lado a lado, paralelismo foi o que fizemos no exemplo anterior, executamos códigos de modo paralelo, ou seja, simultaneamente, já a concorrência aconteceria quando esses códigos paralelos tentassem acessar o mesmo recurso, Rust foi pensado para ser thread safe, ou seja, seguro para trabalhar com threads.

Se tentarmos usar o código abaixo não teremos sucesso em sua compilação.

fn main() {
    use std::thread;
    let mut a = 10;
    thread::spawn(|| {
        a = 20;
    }).join().unwrap();
    thread::spawn(|| {
        println!("a = {}", a);
    }).join().unwrap();
}

Ai você pensa "hm... o compilador disse para eu mover o valor de um lugar para o outro, vou fazer isso".

fn main() {
    use std::thread;
    let mut a = 10;
    thread::spawn(move || {
        a = 20;
    }).join().unwrap();
    thread::spawn(move || {
        println!("a = {}", a);
    }).join().unwrap();
}

Movemos o valor, e a bloqueamos a thread até o fim da execução e... 'a = 10'? Ué. Espera, aprendemos sobre o Rc<T> e sobre o RefCell<T>, vou criar uma referência mutável e compartilhada.

use std::{cell::RefCell, rc::Rc, thread};

fn main() {
    let mut a = Rc::new(RefCell::new(10));
    let t2 = Rc::clone(&a);
    thread::spawn(move || {
        *t2.borrow_mut() = 42;
    }).join().unwrap();
    thread::spawn(move || {
        println!("a = {:?}", a);
    }).join().unwrap();
}

Outro erro:

error[E0277]: `Rc<RefCell<i32>>` cannot be sent between threads safely
   --> src/main.rs:6:19
    |
6   |       let handle1 = thread::spawn(move || {
    |  ___________________^^^^^^^^^^^^^_-
    | |                   |
    | |                   `Rc<RefCell<i32>>` cannot be sent between threads safely
7   | |         *t2.borrow_mut() = 42;
8   | |     });
    | |_____- within this `[closure@src/main.rs:6:33: 8:6]`
    |
    = help: within `[closure@src/main.rs:6:33: 8:6]`, the trait `Send` is not implemented for `Rc<RefCell<i32>>`
note: required because it's used within this closure
   --> src/main.rs:6:33
    |
6   |       let handle1 = thread::spawn(move || {
    |  _________________________________^
7   | |         *t2.borrow_mut() = 42;
8   | |     });
    | |_____^
note: required by a bound in `spawn`
   --> /home/pgjbz/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/thread/mod.rs:653:8
    |
653 |     F: Send + 'static,
    |        ^^^^ required by this bound in `spawn`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `closures` due to previous error

Vamos parar e pensar um pouco... Por que nossa primeira tentativa não deu certo?

Quando utilizamos um tipo que implementa a trait Copy ao ser passada para outro contexto é feita uma cópia inteira de seu valor, ou seja, é feita uma passagem por valor e não por referência, por isso apenas utilizar o move para mover a variável de contexto não da exito no que queremos fazer.

O segundo erro acontece porque os tipos Rc<T> e RefCell<T>, não são tipos seguros para serem mandados através das threads, ou seja, eles não têm segurança para threads. Por isso iremos ver sobre os tipos Arc<T>, Mutex<T>, e RwLock<T> que implementam traits como Send e Sync.

Scope

A partir da versão 1.63.0 do Rust temos um novo modo de usar threads que é utilizando a função scope, essa função basicamente cria um escopo onde podemos criar threads, e manipular os dados, é feito um join automático em todas as threads criadas dentro deste escopo e o compilador do Rust entende que esses dados podem ser usados "sem riscos".

Vamos utilizar o exemplo do Rust Blog e entender o que acontece nele.

use std::thread::scope;

fn main() {
    let mut a = vec![1, 2, 3];
    let mut x = 0;

    scope(|s| {
        s.spawn(|| {
            println!("ola a partir da primeira thread por escopo");
            dbg!(&a);
        });
        s.spawn(|| {
            println!("ola a partir da segunda thread por escopo");
            x += a[0] + a[2];
        });
        println!("ola da thread principal");
    });
    a.push(4);
    assert_eq!(x, a.len());
}

O que acontece é que ao criar o escopo, eu consigo fazer o empréstimo para o escopo, enquanto as threads deste escopo estiverem sendo executadas, eu consigo realizar operações com as variáveis externas sem a necessidade de utilizar o move, como só acessamos a variável x em uma das threads não temos problemas em modificá-la, ao fim do escopo temos acesso novamente as variáveis. Caso tentarmos modificar a variável "x" teremos um problema de ownership, violando a regra de referência exclusiva das referências mutáveis.