멀티스레드 환경에서 안전한 작동에 대한 문제는 오랜 역사를 가지고 있으며, 프로그래머는 레이스 조건, 일관되지 않은 데이터, 메모리 누수 문제를 계속해서 직면해 왔습니다. Rust는 이러한 문제를 컴파일 단계에서 최소화하기 위해 marker-trait인 Send와 Sync를 도입했습니다.
문제 — 스레드 간 공유 데이터에 대한 접근 제어의 부재로 인한 디버깅하기 어려운 오류입니다. 많은 언어에서는 이 책임이 전적으로 프로그래머에게 있지만, Rust의 경우 컴파일러가 스레드 간에 전송 또는 공유할 수 있는지를 검사합니다.
해결책: Send 트레이트는 한 스레드에서 다른 스레드로 객체를 안전하게 전송할 수 있도록 보장합니다. Sync는 여러 스레드에서 객체에 대한 참조를 안전하게 공유할 수 있도록 합니다. Rust의 대부분의 표준 타입은 이러한 트레이트를 자동으로 구현하며, 사용자 정의 타입은 수동으로 구현하거나 특정 경우에 대해 impl !Send 또는 impl !Sync를 통해 이를 금지할 수 있습니다.
코드 예시:
use std::sync::{Arc, Mutex}; use std::thread; 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(); } // counter는 항상 10이 되며 레이스가 없습니다!
주요 특징:
안전하지 않은 포인터를 가진 타입이 Send 또는 Sync가 될 수 있을까요?
아니요, 타입에 raw pointer나 스레드 안전성 보장이 없는 리소스가 포함되어 있을 경우, 그 타입은 이러한 트레이트를 구현하지 않으며, 개발자는 완전한 책임 하에 수동으로 이를 구현해야 합니다 (보통 unsafe impl Send/Sync로).
Rc<T>와 RefCell<T>는 Send 또는 Sync인가요?
아니요, Rc<T>와 RefCell<T>는 멀티스레드 사용에 안전하지 않습니다 (Send도 Sync도 아님). 멀티스레드 상황에서는 Arc<T>와 Mutex/RwLock이 사용됩니다.
static 변수가 구현된 Sync가 아닌 타입을 포함하면 어떻게 되나요?
Rust는 그러한 static 변수가 존재하는 것을 허용하지 않습니다: 이 변수는 Sync여야 하며, 그렇지 않으면 컴파일러가 오류를 발생시킵니다.
젊은 개발자가 객체 Rc를 thread::spawn에 넣습니다 — 코드는 Rc가 스레드 간에 전달되지 않으면 컴파일됩니다. thread::spawn에서 Rc를 꺼내려는 시도는 컴파일 오류를 발생시킵니다. 왜냐하면 Rc는 Send를 구현하지 않으며, 레이스로부터 보호되지 않기 때문입니다.
장점:
단점:
Arc+Mutex를 사용하여 멀티스레드 카운터를 구현합니다. 모든 스레드는 스레드 안전한 인터페이스를 통해 동일한 데이터를 작업합니다. 레이스가 없으며, 코드는 안전하고 견고합니다.
장점:
단점: