`Send`와 `Sync`로 확장 가능한 동시성

SendSync로 확장 가능한 동시성 (Extensible Concurrency with Send and Sync)

재미있는 점은, 이번 장에서 지금까지 이야기한 동시성 기능은 거의 모두 언어가 아니라 표준 라이브러리에 속해 있다는 거예요. 동시성을 다루는 선택지는 언어나 표준 라이브러리에 국한되지 않습니다. 직접 동시성 기능을 만들 수도 있고, 다른 사람이 만든 것을 쓸 수도 있어요.

하지만 언어에 내장되어 표준 라이브러리가 아닌 핵심 동시성 개념이 하나 있는데, 바로 std::marker의 트레이트 SendSync입니다.

출처: The Rust Book - Extensible Concurrency with Send and Sync

스레드 사이에서 소유권 옮기기

Send 마커 트레이트는 이 트레이트를 구현하는 타입의 값 소유권이 스레드 사이에서 옮겨질 수 있음을 나타냅니다. 거의 모든 Rust 타입이 Send를 구현하지만, Rc<T>를 포함한 일부 예외가 있어요. Rc<T>Send를 구현할 수 없는데, 만약 Rc<T> 값을 클론하고 그 클론의 소유권을 다른 스레드로 옮기려 한다면, 두 스레드가 동시에 참조 카운트를 갱신할 수도 있기 때문입니다. 이런 이유로 Rc<T>는 스레드 안전성의 성능 비용을 내고 싶지 않은 단일 스레드 상황을 위해 구현되어 있어요.

따라서 Rust의 타입 시스템과 트레이트 바운드는 실수로 Rc<T> 값을 스레드 간에 안전하지 않게 보내는 일이 절대 없도록 보장합니다. Listing 16-14에서 이렇게 하려 했을 때 우리는 the trait Sendis not implemented forRc<Mutex>`` 오류를 봤죠. Send를 구현하는 Arc<T>로 바꾸자 코드가 컴파일되었어요.

Send 타입만으로 구성된 어떤 타입이든 자동으로 Send로 표시됩니다. 거의 모든 기본 타입이 Send인데, 20장에서 다룰 원시 포인터(raw pointer)는 예외예요.

여러 스레드에서 접근하기

Sync 마커 트레이트는 이 트레이트를 구현하는 타입이 여러 스레드에서 참조돼도 안전함을 나타냅니다. 다시 말해, &T(T에 대한 불변 참조자)가 Send를 구현할 때 어떤 타입 TSync를 구현해요. 즉 그 참조자를 다른 스레드로 안전하게 보낼 수 있다는 뜻이죠. Send와 마찬가지로, 기본 타입은 모두 Sync를 구현하고, Sync를 구현하는 타입만으로 구성된 타입도 Sync를 구현합니다.

스마트 포인터 Rc<T>Send를 구현하지 못하는 이유와 같은 이유로 Sync를 구현하지 않아요. 15장에서 이야기한 RefCell<T> 타입과 관련 Cell<T> 계열 타입도 Sync를 구현하지 않습니다. RefCell<T>가 런타임에 수행하는 빌림 검사 구현은 스레드 안전하지 않거든요. 스마트 포인터 Mutex<T>Sync를 구현하고, "Mutex<T> 공유 접근하기"에서 본 것처럼 여러 스레드가 접근을 공유하는 데 쓸 수 있습니다.

SendSync를 직접 구현하는 것은 안전하지 않다

SendSync 트레이트를 구현하는 다른 타입만으로 구성된 타입도 자동으로 SendSync를 구현하므로, 우리가 직접 이 트레이트들을 구현할 필요가 없어요. 마커 트레이트라서 구현할 메서드조차 없어요. 그저 동시성과 관련된 불변식(invariant)을 강제하는 데 유용할 뿐입니다.

이 트레이트들을 직접 구현하는 일은 안전하지 않은(unsafe) Rust 코드를 구현하는 일을 수반해요. 안전하지 않은 Rust 코드의 사용은 20장에서 다룰 거예요. 지금 중요한 건, SendSync 구성 요소로 만들어지지 않은 새로운 동시성 타입을 만들려면 안전성 보장을 지키기 위해 신중한 사고가 필요하다는 점입니다. "The Rustonomicon"에 이런 보장과 그것을 지키는 방법에 대한 더 많은 정보가 있습니다.

요약 (Summary)

이 책에서 동시성을 이게 마지막이 아니에요. 다음 장은 async 프로그래밍에 초점을 맞추고, 21장의 프로젝트는 여기서 다룬 개념을 이 장의 작은 예시들보다 더 현실적인 상황에서 사용할 거예요.

앞서 언급했듯이 Rust가 동시성을 다루는 방식 중 언어에 속한 부분은 아주 적어서, 동시성 해법 상당수가 크레이트로 구현되어 있어요. 이런 크레이트는 표준 라이브러리보다 빠르게 진화하니, 다중 스레드 상황에서 쓸 최신·최고의 크레이트를 온라인에서 검색해 보세요.

Rust 표준 라이브러리는 메시지 전달용 채널과, 동시성 상황에서 안전하게 쓰이는 Mutex<T>·Arc<T> 같은 스마트 포인터 타입을 제공합니다. 타입 시스템과 빌림 검사기가 이런 해법을 쓰는 코드에 데이터 레이스나 잘못된 참조자가 생기지 않도록 보장해요. 코드가 일단 컴파일되면, 다른 언어에서 흔한 그런 추적하기 어려운 버그 없이 여러 스레드에서 잘 돌아갈 거라는 확신을 가질 수 있어요. 동시성 프로그래밍은 더 이상 두려워할 개념이 아니에요. 담대하게 프로그램을 동시적으로 만들어 보세요!

더 알아보기 (Learn more)