Send와 Sync
그래도 모든 것이 가변성을 물려받지는 않습니다. 어떤 타입들은 여러분이 메모리의 어떤 위치를 변경하면서도 여러 번 가리킬 수 있게 해 줍니다. 이 타입들이 이런 접근들을 동기화로 관리하지 않는다면 절대로 스레드 경계에서 안전하지 않습니다.
러스트는 이런 특징을 Send와 Sync 트레잇을 통해 알아챕니다.
- 어떤 타입이 다른 스레드로 보내도 안전하다면
Send입니다. - 어떤 타입이 스레드 간에 공유해도 안전하다면
Sync입니다 (T는 오직&T가Send인 경우에만Sync입니다).
Send와 Sync는 러스트의 동시성 이야기에 근본이 됩니다. 그런 면에서, 이들이 잘 동작하도록 하는 상당한 양의 특수 장치들이 있습니다. 첫째로, 그리고 가장 중요한 것은, 이들은 불안전한 트레잇입니다.
이 뜻은 이것들을 구현하는 것이 불안전하다는 의미이며, 다른 불안전한 코드가 이 구현들을 신뢰할 수 있다는 말입니다. 이것들이 표시 트레잇이기 때문에 (이것들은 메서드와 같은 연관된 것들이 없습니다),
이것들이 잘 구현되었다는 것은 단지 구현하면 가져야 하는 본질적인 속성을 가졌다는 뜻입니다. Send나 Sync를 잘못 구현하는 것은 미정의 동작을 유발할 수 있습니다.
Send와 Sync는 또한 자동으로 파생되는 트레잇들입니다. 이것이 의미하는 것은, 다른 모든 트레잇과 달리, 어떤 타입이 Send/Sync 타입들로만 이루어져 있다면, 그 타입 또한 Send/Sync라는 것입니다.
거의 모든 기본 타입이 Send이고 Sync이고, 그 결과로 여러분이 상호작용하게 될 거의 모든 타입들은 Send이고 Sync입니다.
대표적인 예외들은 다음과 같습니다:
- 생 포인터들은
Send도Sync도 아닙니다 (안전 장치가 없기 때문이죠). UnsafeCell은Sync가 아닙니다 (그리고 따라서Cell과RefCell도 아닙니다).Rc는Send도Sync도 아닙니다 (참조 횟수가 공유되고, 동기화가 없기 때문입니다).
Rc와 UnsafeCell은 매우 근본적으로 스레드 경계에서 안전하지 않습니다: 동기화되지 않는 가변 공유 상태를 가지기 때문입니다. 하지만 생 포인터들은, 엄밀하게 말해서, 정보 차원에서 스레드 경계 불안전하다고 표시된 것에 가깝습니다.
생 포인터로 무언가 쓸모있는 것을 하려면 역참조를 해야 하는데, 이것은 이미 불안전하죠. 그런 점에서, 생 포인터들이 스레드 경계에서 안전하다고 표시되어도 "괜찮다"고 주장할 수 있습니다.
하지만 이것들이 스레드 경계에서 불안전하다고 표시된 것은 그들을 포함하는 타입들이 자동으로 스레드 경계에서 안전하다는 표시를 받는 것을 방지하기 위해서라는 점이 중요합니다.
그런 타입들은 흔하지 않은, 추적되지 않는 소유권을 가지고 있는데, 그 타입의 제작자가 스레드 안전성에 대해 반드시 깊이 고민했다고 보기는 어렵습니다.
Rc의 경우를 볼 때, 확실히 스레드 경계에서 안전하지 않은 *mut를 포함하는 타입의 좋은 예가 됩니다.
자동으로 파생되지 않는 타입들은 원할 경우 그냥 구현할 수 있습니다:
#![allow(unused)] fn main() { struct MyBox(*mut u8); unsafe impl Send for MyBox {} unsafe impl Sync for MyBox {} }
어떤 타입이 부적절하게 자동으로 Send나 Sync로 파생되는 굉장히 희귀한 경우, Send와 Sync의 구현을 해제할 수도 있습니다:
#![allow(unused)] #![feature(negative_impls)] fn main() { // 어떤 동기화 기초 타입을 위한 어떤 마법적인 의미가 있어요! struct SpecialThreadToken(u8); impl !Send for SpecialThreadToken {} impl !Sync for SpecialThreadToken {} }
그 자체로는 올바르지 않게 Send와 Sync를 파생받는 것이 불가능하다는 것에 주의하세요. 다른 불안전한 코드에 의해 특별한 의미를 부여받은 타입들만 올바르지 않게 Send나 Sync가 됨으로써 문제를 일으킬 가능성이 있습니다.
생 포인터를 사용하는 대부분의 코드는 Send와 Sync가 파생되어도 좋을 만큼 충분한 추상화 안에 갇혀야 합니다. 예를 들어 러스트의 표준 컬렉션들은 모두 Send와 Sync입니다 (Send와 Sync 타입을 포함할 때),
비록 할당과 복잡한 소유권은 관리하기 위해 생 포인터를 많이 사용하긴 했어도 말입니다. 마찬가지로, 이 컬렉션의 대부분의 반복자들은 Send와 Sync인데, 이들이 컬렉션에 대해 많은 부분 &나 &mut와 같이 동작하기 때문입니다.
예제
Box는 여러 가지 이유 때문에 컴파일러 내부의 특별한 타입으로 구현되어 있지만, 우리는 비슷한 동작을 하는 무언가를 만들어서 언제 Send와 Sync를 구현하는 것이 적절한지 예제를 삼을 수 있습니다.
이것을 Carton이라 부르겠습니다.
우리는 먼저 스택에 할당된 값을 받아서 힙으로 옮기는 코드를 작성하겠습니다.
#![allow(unused)] fn main() { pub mod libc { pub use ::std::os::raw::{c_int, c_void}; #[allow(non_camel_case_types)] pub type size_t = usize; extern "C" { pub fn posix_memalign(memptr: *mut *mut c_void, align: size_t, size: size_t) -> c_int; } } use std::{ mem::{align_of, size_of}, ptr, cmp::max, }; struct Carton<T>(ptr::NonNull<T>); impl<T> Carton<T> { pub fn new(value: T) -> Self { // 힙에 하나의 T를 저장하기에 충분한 메모리를 할당합니다. assert_ne!(size_of::<T>(), 0, "영량 타입은 이 예제에서는 다루지 않습니다"); let mut memptr: *mut T = ptr::null_mut(); unsafe { let ret = libc::posix_memalign( (&mut memptr as *mut *mut T).cast(), max(align_of::<T>(), size_of::<usize>()), size_of::<T>() ); assert_eq!(ret, 0, "할당 실패 혹은 올바르지 않은 정렬선"); }; // NonNull은 그저 포인터가 널이 아니도록 강제하는 구조일 뿐입니다. let ptr = { // 안전성: memptr 은 우리가 레퍼런스에서 생성했고 우리가 독점적으로 가지고 // 있기 때문에 역참조할 수 있습니다. ptr::NonNull::new(memptr) .expect("posix_memalign이 0을 반환했으니 널이 아님이 보장됨") }; // 값을 스택에서 우리가 힙에 할당한 위치로 옮깁니다. unsafe { // 안전성: 널이 아니라면, posix_memalign은 유효하게 쓸 수 있고 // 잘 정렬된 포인터를 반환합니다. ptr.as_ptr().write(value); } Self(ptr) } } }
이건 그렇게 쓸모가 있지는 않군요, 우리의 사용자들이 값을 주고 나면 그 값을 접근할 방법이 없네요. Box는 Deref와 DerefMut를 구현해서 안의 값을 접근할 수 있게 합니다.
우리도 이걸 합시다.
#![allow(unused)] fn main() { use std::ops::{Deref, DerefMut}; struct Carton<T>(std::ptr::NonNull<T>); impl<T> Deref for Carton<T> { type Target = T; fn deref(&self) -> &Self::Target { unsafe { // 안전성: 포인터는 [`Self::new`]의 논리에 의해 정렬되어 있고, 초기화되었으며, // 역참조할 수 있습니다. 우리는 이것을 읽는 사람들이 `Carton`을 빌리기를 요구하고, // 반환값의 수명은 입력의 수명과 동기화됩니다. 이것이 의미하는 것은 반환된 레퍼런스가 // 해제될 때까지 아무도 `Carton`의 내용물을 변경하지 못하도록 대여 검사기가 // 강제할 거라는 겁니다. self.0.as_ref() } } } impl<T> DerefMut for Carton<T> { fn deref_mut(&mut self) -> &mut Self::Target { unsafe { // 안전성: 포인터는 [`Self::new`]의 논리에 의해 정렬되어 있고, 초기화되었으며, // 역참조할 수 있습니다. 우리는 이것을 변경하는 사람들이 `Carton`을 가변으로 빌리기를 요구하고, // 반환값의 수명은 입력의 수명과 동기화됩니다. 이것이 의미하는 것은 반환된 가변 레퍼런스가 // 해제될 때까지 아무도 `Carton`의 내용물을 접근하지 못하도록 대여 검사기가 // 강제할 거라는 겁니다. self.0.as_mut() } } } }
마지막으로, 우리의 Carton이 Send인지, 그리고 Sync인지 생각해 봅시다. 어떤 타입이 가변 상태를 독점적 접근을 강제하지 않고 다른 타입과 공유하는 일이 없으면 안전하게 Send가 될 수 있습니다.
각 Carton은 독립된 포인터를 가지고 있으므로, 괜찮은 것 같습니다.
#![allow(unused)] fn main() { struct Carton<T>(std::ptr::NonNull<T>); // 안전성: 우리 말고는 이 생 포인터를 가지고 있지 않으므로, 만약 `T`가 안전하게 다른 스레드로 보낼 수 있다면 // `Carton`도 안전하게 보낼 수 있습니다. unsafe impl<T> Send for Carton<T> where T: Send {} }
Sync는 어떨까요? Carton이 Sync가 되기 위해서 우리는 &Carton에 저장된 내용물이 읽히거나 변경되는 도중 다른 &Carton에 있는 그 내용물을 변경할 수 없다는 것을 강제해야 합니다.
그 포인터에 쓰려면 &mut Carton이 필요하고, 대여 검사기가 그 가변 레퍼런스가 독점적일 것을 강제하므로, Carton을 Sync로 만드는 것에 있어서도 건전성 문제는 없습니다.
#![allow(unused)] fn main() { struct Carton<T>(std::ptr::NonNull<T>); // 안전성: 동기화되지 않고 `&Carton<T>`에서 `&T`로 가는 공개적인 방법이 존재하므로 (`Deref` 같은), // `Carton<T>`는 `T`가 `Sync`가 아니라면 `Sync`가 될 수 없습니다. // 역으로 보면, `Carton` 자체는 아무 내부 가변성도 전혀 사용하지 않습니다: 모든 변경은 독점적 레퍼런스(`&mut`)를 // 통해 이루어지죠. 이것이 의미하는 것은 `T`가 `Sync`면 `Carton<T>`가 `Sync`가 되기에 충분하다는 겁니다: unsafe impl<T> Sync for Carton<T> where T: Sync {} }
우리의 타입이 Send이고 Sync인지 판별할 때 우리는 보통 포함되어 있는 모든 타입이 Send이고 Sync인 것을 강제합니다. 표준 라이브러리 타입처럼 동작하는 수제 타입을 작성할 때 우리는 같은 요구사항을 가지는지 판별할 수 있습니다.
예를 들어, 다음의 코드는 비슷한 Box가 Send가 된다면 Carton이 Send라고 판별합니다 (이 경우에는 T가 Send이면 Box가 Send가 되므로, 결국 조건은 "T가 Send라면" 이라는 말과 같습니다).
#![allow(unused)] fn main() { struct Carton<T>(std::ptr::NonNull<T>); unsafe impl<T> Send for Carton<T> where Box<T>: Send {} }
지금 당장은 Carton<T>은 메모리 누수가 있는데, 할당한 메모리를 절대 해제하지 않기 때문입니다. 이것을 고치면 Send가 되기 위한 새로운 요구사항이 생기게 됩니다:
우리는 다른 스레드에서 할당되어 넘어온 포인터에 free를 호출할 수 있는지 알아야 합니다. libc::free의 문서에서 이것이 사실이라는 것을 확인할 수 있습니다.
#![allow(unused)] fn main() { struct Carton<T>(std::ptr::NonNull<T>); mod libc { pub use ::std::os::raw::c_void; extern "C" { pub fn free(p: *mut c_void); } } impl<T> Drop for Carton<T> { fn drop(&mut self) { unsafe { libc::free(self.0.as_ptr().cast()); } } } }
이런 일이 일어나지 않는 좋은 예는 MutexGuard입니다: 어떻게 이것이 Send가 아닌지를 유의하세요.
MutexGuard의 구현은 여러분이 다른 스레드에서 얻은 락을 해제하려 하지 않는다는 것을 확실히 하도록 요구하는 라이브러리를 사용합니다.
만약 MutexGuard를 다른 스레드로 보낼 수 있다면 소멸자는 여러분이 보낸 그 스레드에서 실행될 것이고, 요구사항은 충족되지 않을 것입니다.
MutexGuard는 그래도 Sync일 수 있는데, 여러분이 다른 스레드로 보낼 수 있는 건 &MutexGuard이고, 레퍼런스를 해제하는 것은 아무 작업도 실행하지 않기 때문입니다.