Send
와 Sync
트레이트를 통한 확장 가능한 동시성
흥미롭게도, 이 장에서 지금까지 다룬 대부분의 동시성 기능은 언어 자체가 아닌 표준 라이브러리의 일부였다. 동시성을 처리하는 방법은 언어나 표준 라이브러리에 국한되지 않는다. 여러분은 직접 동시성 기능을 작성하거나 다른 사람이 작성한 기능을 사용할 수 있다.
그러나 언어 자체에 내장된 주요 동시성 개념 중 하나는 std::marker
트레이트인 Send
와 Sync
이다.
스레드 간 소유권 이전을 허용하는 Send
Send
마커 트레이트는 Send
를 구현한 타입의 값에 대한 소유권이 스레드 간에 전달될 수 있음을 나타낸다. 거의 모든 Rust 타입은 Send
를 구현하지만, 몇 가지 예외가 있다. 예를 들어 Rc<T>
는 Send
를 구현할 수 없다. Rc<T>
값을 복제한 후 복제본의 소유권을 다른 스레드로 전달하려고 하면, 두 스레드가 동시에 참조 카운트를 업데이트할 가능성이 있기 때문이다. 따라서 Rc<T>
는 스레드 안전성을 위한 성능 손실을 감수하고 싶지 않은 단일 스레드 상황에서 사용하도록 구현되었다.
따라서 Rust의 타입 시스템과 트레이트 바운드는 실수로 Rc<T>
값을 스레드 간에 안전하지 않게 전달하는 일이 없도록 보장한다. 리스트 16-14에서 이를 시도했을 때, the trait Send is not implemented for Rc<Mutex<i32>>
라는 오류가 발생했다. Send
를 구현한 Arc<T>
로 전환하자 코드가 정상적으로 컴파일되었다.
Send
타입으로만 구성된 모든 타입은 자동으로 Send
로 표시된다. 원시 포인터를 제외한 거의 모든 기본 타입은 Send
를 구현하며, 원시 포인터에 대해서는 20장에서 자세히 다룰 예정이다.
Sync
를 사용한 다중 스레드 접근 허용
Sync
마커 트레이트는 이 트레이트를 구현한 타입이 여러 스레드에서 참조해도 안전함을 나타낸다. 즉, &T
(T에 대한 불변 참조)가 Send
를 구현한다면, 해당 타입 T
는 Sync
를 구현한다고 볼 수 있다. 이는 참조를 다른 스레드로 안전하게 보낼 수 있음을 의미한다. Send
와 마찬가지로, 모든 기본 타입은 Sync
를 구현하며, Sync
를 구현한 타입으로만 구성된 타입 역시 Sync
를 구현한다.
스마트 포인터 Rc<T>
는 Send
를 구현하지 않는 것과 같은 이유로 Sync
도 구현하지 않는다. RefCell<T>
타입(15장에서 다룬 내용)과 관련된 Cell<T>
타입들도 Sync
를 구현하지 않는다. RefCell<T>
가 런타임에 수행하는 차용 검사는 스레드 안전하지 않다. 반면, 스마트 포인터 Mutex<T>
는 Sync
를 구현하며, “다중 스레드 간 Mutex<T>
공유”에서 본 것처럼 다중 스레드 간 접근을 공유하는 데 사용할 수 있다.
Send
와 Sync
를 수동으로 구현하는 것은 안전하지 않다
Send
와 Sync
트레이트를 구현한 타입들로만 구성된 타입은 자동으로 Send
와 Sync
를 구현한다. 따라서 이 트레이트를 수동으로 구현할 필요가 없다. 마커 트레이트로서, 이들은 구현할 메서드도 없다. 이들은 단지 동시성과 관련된 불변 조건을 강제하는 데 유용하다.
이 트레이트를 수동으로 구현하려면 안전하지 않은 Rust 코드를 구현해야 한다. 안전하지 않은 Rust 코드 사용에 대해서는 20장에서 다룰 예정이다. 지금 중요한 점은 Send
와 Sync
로 구성되지 않은 새로운 동시성 타입을 만들 때, 안전성 보장을 유지하기 위해 신중하게 고민해야 한다는 것이다. “The Rustonomicon”에는 이러한 보장과 이를 유지하는 방법에 대한 더 많은 정보가 있다.
요약
이 책에서 동시성에 대해 다루는 내용은 여기서 끝나지 않는다. 다음 장에서는 비동기 프로그래밍에 초점을 맞추고, 21장의 프로젝트에서는 이 장에서 배운 개념을 더 현실적인 상황에서 활용한다.
앞서 언급했듯이, Rust가 동시성을 처리하는 방식은 언어 자체에 포함된 부분이 거의 없다. 따라서 많은 동시성 솔루션이 크레이트로 구현된다. 이러한 크레이트는 표준 라이브러리보다 더 빠르게 발전하므로, 멀티스레드 상황에서 사용할 최신 크레이트를 온라인에서 검색해보는 것이 좋다.
Rust 표준 라이브러리는 메시지 전달을 위한 채널과 Mutex<T>
, Arc<T>
와 같은 스마트 포인터 타입을 제공한다. 이들은 동시성 컨텍스트에서 안전하게 사용할 수 있다. 타입 시스템과 빌림 검사기는 이러한 솔루션을 사용하는 코드가 데이터 경쟁이나 잘못된 참조로 이어지지 않도록 보장한다. 코드가 컴파일되면, 여러 스레드에서 안정적으로 실행될 것이라는 확신을 가질 수 있다. 다른 언어에서 흔히 발생하는 추적하기 어려운 버그를 걱정할 필요가 없다. 동시성 프로그래밍은 더 이상 두려워할 개념이 아니다. 두려움 없이 프로그램에 동시성을 도입해보자!