Rust에서 스레드 작업의 안전성은 두 개의 자동 트레이트인 Send와 Sync에 의해 보장됩니다.
Send**는 타입을 스레드 간에(소유권 이전을 통해) 전달할 수 있도록 합니다.Sync**는 해당 타입이 여러 스레드에서 동시에 안전하게 사용될 수 있음을 보장합니다(참조를 통해).Rust의 대부분의 표준 타입은 기본적으로 이러한 트레이트를 구현합니다. 예를 들어, Arc<T>, Mutex<T>는 Send 및 Sync를 구현합니다(만약 T도 이러한 트레이트를 준수한다면).
사용자는 자신의 타입에 대해 이러한 트레이트를 명시적으로 금지하거나 구현할 수 있습니다. 예를 들어, 내부에 안전하지 않은 필드(예: 원시 포인터 또는 외부 리소스)가 있는 경우 !Send 또는 !Sync로 설정해야 합니다:
use std::marker::PhantomData; use std::rc::Rc; struct MyType { not_thread_safe: Rc<u32>, _marker: PhantomData<*const ()>, } // Rc<u32>는 Send/Sync를 구현하지 않으므로 MyType도 이러한 트레이트를 구현하지 않습니다.
낮은 수준의 래퍼 및 수동 스레드 안전성 관리 구현 시 Send/Sync를 수동으로 구현할 수 있습니다(unsafe):
unsafe impl Send for MyType {} unsafe impl Sync for MyType {}
스레드 안전성을 보장하는 것은 프로그래머의 책임입니다.
Rc<T>를 여러 스레드로 Arc<Rc<T>>를 통해 이동시키면 어떻게 될까요?
많은 사람들은 Arc가 모든 것을 보호한다고 생각합니다. 하지만 Rc<T>는 Send/Sync를 구현하지 않습니다, Arc로 감싸더라도! 다음과 같습니다:
use std::rc::Rc; use std::sync::Arc; fn main() { let data = Arc::new(Rc::new(5)); // std::thread::spawn(move || { // println!("{:?}", data); // }); // 컴파일러는 이것을 허용하지 않습니다! }
Arc는 내부 구성원에서 Send/Sync의 부족을 보완하지 않습니다.
이야기
프로젝트에서 스레드 간에 데이터를 공유하고 비차단 방식으로 소유권을 나누기 위해 Arc<Rc<T>>를 사용해 보려 했습니다. 프로그램은 예측할 수 없는 동작으로 실행 중에 충돌했습니다; Rc가 스레드 안전하지 않다는 사실을 처음에 알지 못했습니다.
이야기
자작 이벤트 루프에서 State 타입이 데이터에 대한 원시 포인터를 유지했습니다. State 타입에 unsafe impl Send를 표시했지만 동기화 설정을 잊었습니다. 결과적으로 릴리스 후에 발견된 전형적인 데이터 경쟁이 발생했습니다.
이야기
개발자가 Mutex에 대한 newtype 래퍼를 구현했지만 수동으로 Sync를 구현하지 않아서 !Sync로 만들어 버렸습니다. 이것은 Arc<Mutex<T>>와 같은 멀티스레드 컨텍스트에서 타입을 사용하는 데 문제를 일으켰으며, 컴파일러는 Sync를 요구했습니다. unsafe impl Sync를 구현하고 안전성을 분석하여 수정했습니다.