Rust: Programação Concorrente em Rust
Última atualização: 2026-08-26
Concorrência é a capacidade de um programa de realizar várias tarefas “ao mesmo tempo” — o Rust elimina conflitos de acesso aos dados em tempo de compilação por meio de seus sistemas de propriedade e de tipos, permitindo que você escreva código concorrente que seja ao mesmo tempo eficiente e seguro.
Se um programa de thread único é como “uma pessoa trabalhando na cozinha do início ao fim”, então a multithreading é como “vários chefs trabalhando ao mesmo tempo” — alguns cortando legumes, outros refogando e outros ainda montando os pratos. Mas quando há gente demais na cozinha, a situação pode facilmente ficar caótica: duas pessoas podem tentar pegar a mesma faca ao mesmo tempo, ou alguém pode pegar um ingrediente que outra pessoa já está usando. O modelo de concorrência do Rust é como uma “cozinha com vários chefs e regras” — cada chef tem suas próprias ferramentas, os ingredientes são repassados por canais dedicados e os temperos compartilhados só podem ser usados por uma pessoa por vez.
1. O que você vai aprender
std::thread::spawnCriar uma thread ejoinAguardar até que a thread terminemoveOs closures transferem a propriedade para a threadmpsc::channelCanal — Mensagens com vários produtores e um único consumidorMutex<T>Bloqueios de exclusão mútua — Garantindo o acesso seguro entre threads a dados compartilhadosArc<T>Contagem atômica de referências — permitindo queMutex<T>seja compartilhado entre várias threads- O conceito da característica
Send/Sync— A base da segurança de concorrência no Rust
2. A história de um sistema colaborativo de edição de documentos
(1) Problema: Documentação online sem controle de concorrência
A empresa da Luna está desenvolvendo um editor colaborativo de documentos online. As primeiras versões permitiam que apenas uma pessoa editasse por vez, o que gerava reclamações constantes dos membros da equipe:
- Enquanto Alice escrevia a proposta, Bob também estava editando a mesma seção do texto — as edições de ambos se sobrescreveram.
- Carol quer ver as atualizações do documento em tempo real, mas precisa atualizar a página toda vez.
- Dave enviou uma alteração, mas Luna não fazia ideia de quem a havia feito.
- Para piorar a situação, dois editores editaram o mesmo conteúdo ao mesmo tempo, e o documento travou.
- Sempre que a Luna tenta adicionar um recurso de “edição mutuamente exclusiva”, o código fica uma bagunça — cheio de variáveis globais e bloqueios.
“Seria ótimo se pudéssemos criar um tópico separado para cada editor, enviar solicitações de modificação por meio de um canal e proteger o conteúdo do documento com um mutex...”
(2) Abordagens ao modelo de concorrência do Rust
use std::thread;
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
fn main() {
// Create a document with shared content(Arc<Mutex<String>>)
let document = Arc::new(Mutex::new(String::from("# Collaborative Documents\n\n")));
// Create a channel,Used to transmit edit requests
let (tx, rx) = mpsc::channel::<String>();
// Receiving Thread:Processing editing requests on an ongoing basis
let doc_for_receiver = Arc::clone(&document);
let receiver = thread::spawn(move || {
for edit in rx {
let mut doc = doc_for_receiver.lock().unwrap();
doc.push_str(&edit);
doc.push('\n');
println!("[Receiver] Edits Applied");
}
});
// Simulating an Editor Thread
let tx1 = tx.clone();
thread::spawn(move || {
tx1.send("- Alice: Added the content for Chapter 1".to_string()).unwrap();
});
let tx2 = tx.clone();
thread::spawn(move || {
tx2.send("- Bob: The title of Chapter 2 has been revised.".to_string()).unwrap();
});
// The main thread also sends a message
tx.send("- Luna: Format Adjustments".to_string()).unwrap();
// Wait until all editors have finished sending their edits
thread::sleep(Duration::from_millis(100));
drop(tx); // Close the sender
receiver.join().unwrap();
// Final Document Content
let final_doc = document.lock().unwrap();
println!("\n=== Final Document ===");
println!("{}", *final_doc);
}
A solução de concorrência do Rust separa claramente as responsabilidades: cada editor é executado em uma thread separada, as solicitações de edição são transmitidas por meio do
mpsc::channele o conteúdo do documento é protegido peloMutex. OArcpermite que oMutexseja compartilhado entre várias threads. O sistema de propriedade garante que não ocorram condições de corrida.
3. Conceitos fundamentais
(1) A Estrutura de Concorrência do Rust
graph TB
A[Rust Concurrent Programming] --> B[Thread Management]
A --> C[Messaging]
A --> D[Shared Status]
A --> E[Safety Guarantee]
B --> B1["thread::spawn ||"]
B --> B2["join() The Wait Is Over"]
B --> B3["move Closures Transfer Ownership"]
C --> C1["mpsc::channel"]
C --> C2["Sender / Receiver"]
C --> C3["send / recv"]
D --> D1["Mutex<T> Mutex"]
D --> D2["lock() Acquire a lock"]
D --> D3["Arc<T> Atomic Reference Counting"]
E --> E1["Send: Ownership can be transferred across threads"]
E --> E2["Sync: References can be shared across threads"]
E --> E3["Eliminating Data Races at Compile Time"]
(2) Comparação entre três modelos de concorrência
| Recurso | thread::spawn (thread) | mpsc::channel (canal) | Arc<Mutex<T>> (estado compartilhado) |
|---|---|---|---|
| Conceito central | Unidades de execução independentes | Passagem de mensagens | Memória compartilhada + bloqueios |
| Métodos de transferência de dados | move: Transferir propriedade por meio de um closure | send/recv: Enviar uma mensagem | lock: Obter um valor interno |
| Casos de uso | Execução paralela de tarefas independentes | Padrão produtor-consumidor | Várias threads acessando os mesmos dados |
| Vantagens | Aproveita ao máximo as CPUs multi-core | Separa emissores e receptores | Compartilha diretamente qualquer tipo |
| Desvantagens | Comunicação complexa entre threads | Não é adequado para transferências frequentes de pequenos volumes de dados | Possíveis impasses e sobrecarga de desempenho |
| Recursos do Rust | A propriedade evita ponteiros pendentes | O compilador garante o uso correto | Evita corridas de dados em tempo de compilação |
(3) Característica “Enviar e sincronizar”
| Característica | Significado | Condições implementadas automaticamente | Tipos não implementados automaticamente |
|---|---|---|---|
| Enviar | A propriedade de um tipo pode ser transferida entre threads | Implementado automaticamente para a grande maioria dos tipos | Rc<T> (contagem de referências não atômica) |
| Sincronização | As referências aos tipos podem ser compartilhadas entre threads | Implementada automaticamente para a grande maioria dos tipos | RefCell<T> (mutabilidade interna não atômica) |
| T: Enviar + Sincronizar | Tipos que podem ser compartilhados e passados com segurança entre threads | Arc<T>, Mutex<T> |
Ponteiros brutos *const T, *mut T |
(4) Referência rápida para a seleção de métodos de comunicação entre threads
| Método de comunicação | Tipo | Direção do fluxo de dados | Bloqueio necessário | Cenários aplicáveis |
|---|---|---|---|---|
| Canal | mpsc::channel |
Unidirecional (Remetente → Destinatário) | Não | Padrão produtor-consumidor |
| Canal (vários produtores) | mpsc::channel + tx.clone() |
Multi→Único | Não | Resultados agregados em multithread |
| Status de compartilhamento | Arc<Mutex<T>> |
Bidirecional | Sim (bloqueio de exclusão mútua) | Acesso multithread de leitura/gravação aos mesmos dados |
| Status de compartilhamento (Leitura/Gravação) | Arc<RwLock<T>> |
Bidirecional | Sim (bloqueio de leitura/gravação) | Cenário com muitas leituras e poucas gravações |
| Operações atômicas | AtomicUsize et al. |
Bidirecional | Não (no nível do hardware) | Contadores/sinalizadores simples |
| Barreira | Barrier |
Ponto de sincronização | Não | Ponto de convergência multithread |
O Rust não depende de verificações em tempo de execução para garantir a segurança entre threads; em vez disso, ele realiza verificações em tempo de compilação usando as duas características marcadoras
SendeSync. Se um tipo não forSend, a tentativa de passá-lo para outra thread resultará em um erro em tempo de compilação.
4. Exemplos de programação concorrente
▶ Exemplo 1: thread::spawn + join — Criando e aguardando uma thread (Dificuldade ⭐)
// ============================================
// Demo:thread::spawn Create a Thread、join The Wait Is Over
// Simulation: Several chefs are preparing different dishes at the same time in the kitchen.
// ============================================
use std::thread;
use std::time::Duration;
fn main() {
println!("=== Kitchen Renovation Begins ===");
// --- 1. Create three threads to perform different tasks ---
let chef1 = thread::spawn(|| {
for i in 1..=3 {
println!("[Chef A] Chopping vegetables... Cut #{}", i);
thread::sleep(Duration::from_millis(50));
}
"A Finished chopping the vegetables"
});
let chef2 = thread::spawn(|| {
for i in 1..=3 {
println!("[Chef B] Stir-frying... Step #{}", i);
thread::sleep(Duration::from_millis(40));
}
"B Finished cooking the stir-fry"
});
// The main thread is also running
for i in 1..=3 {
println!("[Head Chef] Plating in progress... Item #{}", i);
thread::sleep(Duration::from_millis(60));
}
// --- 2. join Wait for all threads to finish and retrieve the return values ---
let result1 = chef1.join().unwrap();
let result2 = chef2.join().unwrap();
println!("\n=== Kitchen Shutdown ===");
println!("Chef A: {}", result1);
println!("Chef B: {}", result2);
println!("All work has been completed!");
}
Resultado:
=== Kitchen Renovation Begins ===
[Chef A] Chopping vegetables... Cut #1
[Chef B] Stir-frying... Step #1
[Head Chef] Plating in progress... Item #1
[Chef A] Chopping vegetables... Cut #2
[Chef B] Stir-frying... Step #2
[Head Chef] Plating in progress... Item #2
[Chef A] Chopping vegetables... Cut #3
[Chef B] Stir-frying... Step #3
[Head Chef] Plating in progress... Item #3
=== Kitchen Shutdown ===
Chef A: A Finished chopping the vegetables
Chef B: B Finished cooking the stir-fry
All work has been completed!
thread::spawnAceita um closure e o executa em uma nova thread do sistema operacional.join()Bloqueia a thread atual até que a thread de destino seja concluída e retornaResult<T>— se a thread entrar em pânico,join()retornaErr. A ordem em que as threads são executadas é determinada pelo agendador do sistema operacional e pode variar de execução para execução.
▶ Exemplo 2: mpsc::channel — Mensagens (Dificuldade ⭐⭐)
// ============================================
// Demo: mpsc::channel Many-Producer, Single-Consumer Messaging
// Simulation: Multiple editors send modification requests to the document server
// ============================================
use std::thread;
use std::sync::mpsc;
use std::time::Duration;
#[derive(Debug)]
enum EditAction {
Insert { user: String, text: String },
Delete { user: String, line: u32 },
Format { user: String, style: String },
}
fn main() {
println!("=== Collaborative Document Editor ===");
// Create a Channel:Sender Can be cloned,Receiver It is the only one
let (tx, rx) = mpsc::channel::<EditAction>();
// --- 1. Start the receive thread (Document Server) ---
let receiver = thread::spawn(move || {
for action in rx {
match &action {
EditAction::Insert { user, text } => {
println!("[Server] {} Inserted: {}", user, text);
}
EditAction::Delete { user, line } => {
println!("[Server] {} Deleted line {}", user, line);
}
EditAction::Format { user, style } => {
println!("[Server] {} Formatting has been applied: {}", user, style);
}
}
// Simulated Processing Time
thread::sleep(Duration::from_millis(20));
}
println!("[Server] Passage Closed,Unsubscribe");
});
// --- 2. Create the first editor thread (Alice) ---
let tx1 = tx.clone();
let editor1 = thread::spawn(move || {
tx1.send(EditAction::Insert {
user: "Alice".to_string(),
text: "Chapter 1: Rust Introduction".to_string(),
}).unwrap();
thread::sleep(Duration::from_millis(10));
tx1.send(EditAction::Format {
user: "Alice".to_string(),
style: "Bold the title".to_string(),
}).unwrap();
});
// --- 3. Create the second editor thread (Bob) ---
let tx2 = tx.clone();
let editor2 = thread::spawn(move || {
tx2.send(EditAction::Insert {
user: "Bob".to_string(),
text: "Rust is a systems programming language".to_string(),
}).unwrap();
thread::sleep(Duration::from_millis(10));
tx2.send(EditAction::Delete {
user: "Bob".to_string(),
line: 1,
}).unwrap();
});
// --- 4. The main thread also sends a message ---
tx.send(EditAction::Insert {
user: "System".to_string(),
text: "Auto-Save Documents".to_string(),
}).unwrap();
// Wait for the editor thread to finish
editor1.join().unwrap();
editor2.join().unwrap();
// Close the sender - After all Senders are dropped, the Receiver for loop will end automatically
// tx is dropped here (because tx is the last sender on the main thread)
println!("All editors have completed their work");
receiver.join().unwrap();
println!("=== End of editing session ===");
}
// Note: tx is automatically dropped when leaving the scope, channel closes
// If you need to explicitly turn it off,Can be used drop(tx)
Resultado:
=== Collaborative Document Editor ===
[Server] System Inserted: Auto-Save Documents
[Server] Alice Inserted: Chapter 1: Rust Introduction
[Server] Bob Inserted: Rust is a systems programming language
[Server] Alice Formatting has been applied: Bold the title
[Server] Bob Deleted line 1
All editors have completed their work
[Server] Passage Closed,Unsubscribe
=== End of editing session ===
mpscsignifica “Múltiplos produtores, único consumidor”.Senderpode ser copiado para várias threads por meio declone, mas só pode haver umReceiver. Quando todos osSendersão descartados, o canal se fecha automaticamente e o iterador deReceiveré encerrado.sendretornaResult— se o lado receptor tiver sido fechado, retornaErr.
▶ Exemplo 3: Arc<Mutex<T>> — Acesso seguro entre threads a um estado compartilhado (Dificuldade ⭐⭐⭐)
// ============================================
// Demo: Arc<Mutex<T>> Safely Sharing Data Among Multiple Threads
// Simulation: Multiple workers modifying a shared counter simultaneously
// ============================================
use std::thread;
use std::sync::{Arc, Mutex};
use std::time::Duration;
fn main() {
println!("=== Shared Counter Demo ===");
// Use Arc<Mutex<i32>> to wrap shared data
let counter = Arc::new(Mutex::new(0i32));
let mut handles = vec![];
// --- Start 5 threads,Each thread increments the counter by 10 ---
for id in 0..5 {
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
for _ in 0..10 {
// lock() Acquire a mutex lock——If the lock is held by another thread,The current thread will block while waiting
let mut num = counter_clone.lock().unwrap();
*num += 1;
println!("[Workers{}] Current Count: {}", id, *num);
// Locks are automatically released when they go out of scope
}
println!("[Workers{}] Work Completed", id);
});
handles.push(handle);
}
// --- Wait for all threads to finish ---
for handle in handles {
handle.join().unwrap();
}
// --- Read the final results ---
let final_count = counter.lock().unwrap();
println!("\n=== Final Results ===");
println!("Final counter value: {}", *final_count);
println!("Expected value: {} (5 threads x 10 times)", 5 * 10);
}
Resultado:
=== Shared Counter Demo ===
[Workers0] Current Count: 1
[Workers0] Current Count: 2
[Workers1] Current Count: 3
[Workers1] Current Count: 4
[Workers0] Current Count: 5
... (The intermediate output varies depending on thread scheduling)
[Workers4] Current Count: 50
[Workers4] Work Completed
=== Final Results ===
Final counter value: 50
Expected value: 50 (5 threads x 10 times)
Mutex<T>oferece exclusão mútua — apenas uma thread pode acessar os dados internos por vez.lock()retornaMutexGuard<T>, que implementaDerefeDrop: o bloqueio é liberado automaticamente ao sair do escopo.Arc<T>(Contagem de Referências Atômica) é a versão multithread deRc<T>, utilizando operações atômicas para garantir a segurança entre threads na contagem de referências.Arc::cloneincrementa a contagem de referências sem copiar dados.
▶ Exemplo 4: Colaboração entre threads — O padrão produtor-consumidor (Dificuldade ⭐⭐⭐)
// ============================================
// Producer-Consumer: Multiple producers + Single consumer
// ============================================
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
let tx2 = tx.clone();
thread::spawn(move || {
let items = vec!["Apple", "Banana", "Orange"];
for item in items {
tx.send(format!("Producer1: {}", item)).unwrap();
thread::sleep(Duration::from_millis(100));
}
println!("Producer1 Done");
});
thread::spawn(move || {
let items = vec!["Watermelon", "Grapes"];
for item in items {
tx2.send(format!("Producer2: {}", item)).unwrap();
thread::sleep(Duration::from_millis(150));
}
println!("Producer2 Done");
});
println!("=== Consumer Receipt ===");
for msg in rx {
println!(" Received: {}", msg);
}
println!("All producers have finished, consumer section ended");
}
Saída (a ordem pode variar):
=== Consumer Receipt ===
Received: Producer1: Apple
Received: Producer2: Watermelon
Received: Producer1: Banana
Received: Producer1: Orange
Received: Producer2: Grapes
Producer1 Done
Producer2 Done
All producers have finished, consumer section ended
tx.clone()Cria vários emissores para implementar um padrão multiprodutor. Quando todos os emissores (txetx2) são removidos, o iteradorrxé encerrado automaticamente — não há necessidade de enviar manualmente um sinal de “fim”.
▶ Exemplo 5: Exercício abrangente — Processamento paralelo de dados (Dificuldade ⭐⭐⭐)
// ============================================
// Parallel Computing: Multithreaded Sharded Summation
// ============================================
use std::sync::{Arc, Mutex};
use std::thread;
fn parallel_sum(data: &[i64], num_threads: usize) -> i64 {
let chunk_size = (data.len() + num_threads - 1) / num_threads;
let result = Arc::new(Mutex::new(0i64));
let mut handles = Vec::new();
for i in 0..num_threads {
let chunk = data[i * chunk_size..(i * chunk_size + chunk_size).min(data.len())].to_vec();
let result = Arc::clone(&result);
handles.push(thread::spawn(move || {
let partial: i64 = chunk.iter().sum();
*result.lock().unwrap() += partial;
partial
}));
}
let mut partials = Vec::new();
for handle in handles {
partials.push(handle.join().unwrap());
}
println!("The various thread sections and: {:?}", partials);
*result.lock().unwrap()
}
fn main() {
let data: Vec<i64> = (1..=1000).collect();
let sequential_sum: i64 = data.iter().sum();
println!("=== Serial Summation ===");
println!("1 to 1000 sum: {}", sequential_sum);
println!("\n=== Parallel Summation (4 Thread) ===");
let parallel_result = parallel_sum(&data, 4);
println!("Parallel Summation Results: {}", parallel_result);
assert_eq!(sequential_sum, parallel_result);
let data2: Vec<i64> = (1..=10_000_000).collect();
let start = std::time::Instant::now();
let _ = data2.iter().sum::<i64>();
let seq_time = start.elapsed();
let start = std::time::Instant::now();
let _ = parallel_sum(&data2, 8);
let par_time = start.elapsed();
println!("\n=== 10M Data Performance Comparison ===");
println!("Serial: {:?}", seq_time);
println!("Parallel (8Thread): {:?}", par_time);
}
Resultado:
=== Serial Summation ===
1 to 1000 sum: 500500
=== Parallel Summation (4 Thread) ===
The various thread sections and: [78126, 218874, 109374, 94126]
Parallel Summation Results: 500500
=== 10M Data Performance Comparison ===
Serial: [Time]
Parallel (8Thread): [Time]
parallel_sumDivida os dados; cada thread calcula uma soma parcial, que é então acumulada no resultado total por meio deArc<Mutex<i64>>.chunk_sizeArredonde para cima para garantir que todos os dados sejam considerados.assert_eq!Verifique se o resultado paralelo corresponde ao resultado serial. Para grandes conjuntos de dados, o uso de multithreading pode melhorar significativamente o desempenho.
❓ Perguntas Frequentes
P:
thread::spawnQual é a relação entre as threads criadas e a thread principal? R: Todas as threads são executadas em paralelo; quando a thread principal é encerrada, todo o programa termina.spawncria threads independentes do sistema operacional. Se a thread principal for encerrada antes que uma thread filha termine, a thread filha será encerrada à força. Portanto, geralmente é necessário usarjoin()para aguardar a conclusão de todas as threads filhas. Um panic em uma thread filha não afetará as outras threads.
P: As funções
mpsc::channel,senderecvbloqueiam? R: A funçãosendnormalmente não bloqueia (ela retorna imediatamente quando há espaço no buffer), enquanto a funçãorecvbloqueia até que uma mensagem seja recebida.mpsc::channelcria um canal limitado (que, na verdade, é um buffer infinito).sendretornaErrquando o lado receptor é fechado.recvbloqueia a thread quando não há mensagens disponíveis e retornaErrquando o canal é fechado.try_recvoferece uma versão não bloqueante.
P: O
Mutex<T>e oArc<Mutex<T>>precisam ser usados juntos? R: Não necessariamente. Se oMutex<T>for usado em uma única thread, oArcnão é necessário. No entanto, em um cenário multithread, cada thread precisa da propriedade ou de uma referência a umMutex— oArcfornece propriedade compartilhada (contagem de referências), permitindo que múltiplas threads mantenham um identificador para o mesmoMutex. Se você usarRc<Mutex<T>>, o compilador reportará um erro (Rcnão éSend).
P: O uso de
Mutexpode causar um impasse? R: O Rust não impede impasses; você deve evitá-los em seu código. Cenários comuns de impasse: duas threads, cada uma detendo um bloqueio, aguardam o bloqueio da outra. A biblioteca padrão do Rust não oferece detecção automática de impasses — isso é de responsabilidade do programador. Estratégias para reduzir impasses incluem: manter uma ordem consistente de bloqueio, usartry_lockem vez delocke minimizar o escopo dos bloqueios tanto quanto possível.
P: Qual é a diferença entre
SendeSync? R:Sendsignifica “a propriedade pode ser transferida entre threads”, eSyncsignifica “as referências podem ser compartilhadas entre threads”. Ambas são características inseguras (implementadas automaticamente; não é necessária nenhuma implementação manual).Rc<T>não éSend(contagem de referências não atômica), eRefCell<T>não éSync(as verificações de empréstimo em tempo de execução não são seguras entre threads). A grande maioria dos tipos da biblioteca padrão é tantoSendquantoSync.
📖 Resumo
thread::spawnCria uma thread do sistema operacional e aceita um closure como corpo de execução;join()aguarda a conclusão da thread e recupera o resultadomoveClosures Transferir a propriedade dos valores para novas threads a fim de evitar referências pendentes — essa é uma aplicação direta do sistema de propriedade do Rust na concorrência.mpsc::channelfornece um canal com múltiplos produtores e um único consumidor;sendenvia mensagens erecvrecebe mensagens; o canal é fechado automaticamente quando todos os remetentes são removidosMutex<T>garante a exclusão mútua;lock()adquire o bloqueio (bloqueia);MutexGuardlibera automaticamente o bloqueio quando sai do escopoArc<T>é a versão segura para múltiplas threads doRc<T>; ela utiliza operações atômicas para gerenciar contagens de referência e funciona como uma ponte para o compartilhamento doMutex<T>entre várias threads.Send/Syncsão traits marcadores de segurança de concorrência do Rust —Sendpermite a transferência de propriedade entre threads,Syncpermite o compartilhamento de referências entre threads, e o compilador realiza verificações em tempo de compilação.
📝 Exercícios
- Dificuldade ⭐: Escreva um programa que crie três threads para calcular as somas de 1..=10, 11..=20 e 21..=30, respectivamente. A thread principal usa
joinpara aguardar a conclusão de todas as threads; em seguida, agrega as três somas parciais e exibe o resultado final. - Dificuldade ⭐⭐: Use
mpsc::channelpara implementar um “distribuidor de tarefas”. Crie 1 thread produtor (que gera 10 tarefas numeradas de 1 a 10) e 2 threads consumidores (cada consumidor recebe um número de tarefa do canal e exibe “[Consumidor X] processando tarefa nº N”). Certifique-se de que todas as tarefas sejam processadas. - Dificuldade ⭐⭐⭐: Implemente um sistema de “conta bancária compartilhada”. Use
Arc<Mutex<f64>>como saldo compartilhado. Crie 4 threads para simular operações de depósito (cada thread deposita um valor aleatório entre 10 e 100 yuan). A thread principal aguarda a conclusão de todas as threads de depósito antes de ler o saldo final. Requisito adicional: Imprima a variação do saldo antes e depois de cada depósito para verificar se não há condição de corrida (saldo final = saldo inicial + soma de todos os depósitos).