SendSync 트레이트를 통한 확장 가능한 동시성

흥미롭게도, 이 장에서 지금까지 다룬 대부분의 동시성 기능은 언어 자체가 아닌 표준 라이브러리의 일부였다. 동시성을 처리하는 방법은 언어나 표준 라이브러리에 국한되지 않는다. 여러분은 직접 동시성 기능을 작성하거나 다른 사람이 작성한 기능을 사용할 수 있다.

그러나 언어 자체에 내장된 주요 동시성 개념 중 하나는 std::marker 트레이트인 SendSync이다.

스레드 간 소유권 이전을 허용하는 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를 구현한다면, 해당 타입 TSync를 구현한다고 볼 수 있다. 이는 참조를 다른 스레드로 안전하게 보낼 수 있음을 의미한다. Send와 마찬가지로, 모든 기본 타입은 Sync를 구현하며, Sync를 구현한 타입으로만 구성된 타입 역시 Sync를 구현한다.

스마트 포인터 Rc<T>Send를 구현하지 않는 것과 같은 이유로 Sync도 구현하지 않는다. RefCell<T> 타입(15장에서 다룬 내용)과 관련된 Cell<T> 타입들도 Sync를 구현하지 않는다. RefCell<T>가 런타임에 수행하는 차용 검사는 스레드 안전하지 않다. 반면, 스마트 포인터 Mutex<T>Sync를 구현하며, “다중 스레드 간 Mutex<T> 공유”에서 본 것처럼 다중 스레드 간 접근을 공유하는 데 사용할 수 있다.

SendSync를 수동으로 구현하는 것은 안전하지 않다

SendSync 트레이트를 구현한 타입들로만 구성된 타입은 자동으로 SendSync를 구현한다. 따라서 이 트레이트를 수동으로 구현할 필요가 없다. 마커 트레이트로서, 이들은 구현할 메서드도 없다. 이들은 단지 동시성과 관련된 불변 조건을 강제하는 데 유용하다.

이 트레이트를 수동으로 구현하려면 안전하지 않은 Rust 코드를 구현해야 한다. 안전하지 않은 Rust 코드 사용에 대해서는 20장에서 다룰 예정이다. 지금 중요한 점은 SendSync로 구성되지 않은 새로운 동시성 타입을 만들 때, 안전성 보장을 유지하기 위해 신중하게 고민해야 한다는 것이다. “The Rustonomicon”에는 이러한 보장과 이를 유지하는 방법에 대한 더 많은 정보가 있다.

요약

이 책에서 동시성에 대해 다루는 내용은 여기서 끝나지 않는다. 다음 장에서는 비동기 프로그래밍에 초점을 맞추고, 21장의 프로젝트에서는 이 장에서 배운 개념을 더 현실적인 상황에서 활용한다.

앞서 언급했듯이, Rust가 동시성을 처리하는 방식은 언어 자체에 포함된 부분이 거의 없다. 따라서 많은 동시성 솔루션이 크레이트로 구현된다. 이러한 크레이트는 표준 라이브러리보다 더 빠르게 발전하므로, 멀티스레드 상황에서 사용할 최신 크레이트를 온라인에서 검색해보는 것이 좋다.

Rust 표준 라이브러리는 메시지 전달을 위한 채널과 Mutex<T>, Arc<T>와 같은 스마트 포인터 타입을 제공한다. 이들은 동시성 컨텍스트에서 안전하게 사용할 수 있다. 타입 시스템과 빌림 검사기는 이러한 솔루션을 사용하는 코드가 데이터 경쟁이나 잘못된 참조로 이어지지 않도록 보장한다. 코드가 컴파일되면, 여러 스레드에서 안정적으로 실행될 것이라는 확신을 가질 수 있다. 다른 언어에서 흔히 발생하는 추적하기 어려운 버그를 걱정할 필요가 없다. 동시성 프로그래밍은 더 이상 두려워할 개념이 아니다. 두려움 없이 프로그램에 동시성을 도입해보자!