동시성
이해
동시성/병렬성은 메시지 전달(Message-passing) 또는 상태 공유(Shared-state)로 구현할 수 있습니다.
스레드
동시성에서는 다음 사항에 주의해야 합니다.
- 데이터 경쟁 스레드가 일관되지 않은 순서로 데이터나 리소스에 접근하는 경쟁 상태
- 교착 상태 두 스레드가 서로 가진 리소스 사용을 끝내기를 기다려 어느 쪽도 계속 진행하지 못하는 교착 상태
- 재현하기 어려운 BUG 특정 상황에서만 발생해 안정적으로 재현하고 수정하기 어려운 버그 Green Thread는 프로그래밍 언어 수준의 thread이며 운영 체제가 제공하는 thread와 완전히 같지 않습니다.
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| { // 注意这里的闭包如果需要入参,需要显式声明 move
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1)); // 此时操作系统有机会切换执行其他线程
}
});
handle.join().unwrap(); // 使用 join 等待所有线程执行结束(否则后续代码不执行)
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
메시지 전달
- Channel 기본 문법
use std::sync::mpsc; // multiple producer single consumer fn main() { let (tx, rx) = mpsc::channel(); // transmitter, receiver thread::spawn(move || { let val = String::from("hi"); tx.send(val).unwrap(); // 此时 val moved ownership }); let received = rx.recv().unwrap(); // 如果使用 try_recv 则不会阻塞 println!("Got: {}", received); } - 다중 producer/transmitter 및 다중 메시지 수신
use std::sync::mpsc; use std::thread; use std::time::Duration; fn main() { let (tx, rx) = mpsc::channel(); // 因为后续闭包内会 move tx 的 ownership // 所以这里需要提前 clone 出 tx1 实现 multi producer let tx1 = tx.clone(); thread::spawn(move || { let vals = vec![ String::from("hi"), String::from("from"), String::from("the"), String::from("thread"), ]; for val in vals { tx1.send(val).unwrap(); thread::sleep(Duration::from_secs(1)); } }); thread::spawn(move || { let vals = vec![ String::from("more"), String::from("messages"), String::from("for"), String::from("you"), ]; for val in vals { tx.send(val).unwrap(); thread::sleep(Duration::from_secs(1)); } }); // 注意较于上例,使用了 for-in loop for received in rx { println!("Got: {}", received); } }
상태 공유
Mutex(Mutual Exclusion, 상호 배제)은 한 순간에 하나의 스레드만 특정 데이터에 접근하도록 합니다. 스레드는 mutex 안의 데이터에 접근하려면 lock을 얻어야 합니다. lock은 mutex의 일부로서 누가 데이터에 배타적으로 접근할 권한이 있는지 계속 추적합니다. 따라서 mutex는 잠금 시스템으로 보유한 데이터를 보호한다고 설명합니다. 다음 두 규칙 때문에 Mutex는 사용하기 어렵습니다:
- 데이터를 사용하기 전에 lock을 얻어야 합니다. 데이터를 사용하기 전에 lock 획득을 시도해야 합니다.
- mutex가 보호하는 데이터를 다 사용하면 다른 스레드가 lock을 얻을 수 있도록 해제해야 합니다. mutex가 보호하는 데이터를 다 사용하면 다른 스레드가 lock을 얻을 수 있도록 잠금을 해제해야 합니다. mutex를 관리하기는 어려워서 많은 사람이 channels를 선호합니다. 그러나 Rust의 형식 시스템과 소유권 규칙은 잠금을 잘못 획득하거나 해제하지 않도록 보장합니다.
- Mutex 기본 문법
use std::sync::Mutex; fn main() { let m = Mutex::new(5); { let mut num = m.lock().unwrap(); // 调用 lock 获取锁 *num = 6; } println!("m = {:?}", m); }Mutex<T>는 스마트 포인터입니다. 정확히 말하면lock은LockResult를 반환하고, 이어지는unwrap은 스마트 포인터MutexGuard를 반환합니다.MutexGuard는 내부 데이터를 가리키는Dereftrait과 scope를 벗어날 때 lock을 자동 해제하는Droptrait을 구현합니다. 따라서 lock 해제를 잊거나 다른 스레드의 사용을 막을 위험이 없으며 lock 해제는 자동으로 일어납니다. - 여러 스레드에서 Mutex 공유
Atomic Reference Counted Type이며 원자 기능이 추가된
Rc<T>로 이해할 수 있습니다.use std::sync::{Arc, Mutex}; use std::thread; fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Result: {}", *counter.lock().unwrap()); }
Send & Sync trait
Send trait을 구현한 형식은 ownership을 스레드 사이에서 전달할 수 있습니다.
Sync trait을 구현한 형식은 여러 스레드에서 접근할 수 있습니다.
Sync에는 Send의 특성이 포함되어 있음을 알 수 있습니다.
Origin
…
Arc<T>를 사용한 원자적 참조 카운팅
다행히 Arc<T>는 동시성 상황에서 안전하게 사용할 수 있는 Rc<T>와 같은 형식입니다. The a stands for atomic, meaning it’s an atomically reference counted type. Atomics are an additional kind of concurrency primitive that we won’t cover in detail here: see the standard library documentation for [std::sync::atomic](https://doc.rust-lang.org/std/sync/atomic/index.html) for more details. At this point, you just need to know that atomics work like primitive types but are safe to share across threads.
그렇다면 왜 모든 기본 형식이 원자적이지 않고 표준 라이브러리 형식이 기본적으로 Arc<T>를 사용하지 않는지 궁금할 수 있습니다. The reason is that thread safety comes with a performance penalty that you only want to pay when you really need to. If you’re just performing operations on values within a single thread, your code can run faster if it doesn’t have to enforce the guarantees atomics provide.
예제로 돌아가 보겠습니다: Arc<T> and Rc<T> have the same API, so we fix our program by changing the use line, the call to new, and the call to clone. The code in Listing 16-15 will finally compile and run:
…