rokdocs
v0.1

3.4. thread:: - OS 스레드 (1:1 모델)

rok::std::thread 모듈은 OS-level 스레드를 다루기 위한 진입점입니다. M:N 협력적 스케줄러 위에서 동작하는 task 와는 2.18 task 에서 정리한 대로 별도의 추상이며, 이 모듈은 native thread 를 직접 spawn 하는 경로를 제공합니다.

3.4.1. 언제 thread vs task

어떤 상황에서 어느 쪽을 골라야 하는지는 다음 표로 가늠할 수 있습니다.

자리권장
일반 동시성 / async I/Otask::spawn (가벼움)
CPU-bound 계산task::spawn 또는 par::* (work-stealing)
Blocking C 라이브러리 호출thread::spawn (task scheduler block 회피)
OS thread API 가 요구되는 자리thread::spawn (TLS, signal mask, etc.)
임베디드 / no-taskthread::spawn

요컨대 task 가 충분히 가볍고 표현력 있게 처리할 수 있는 영역에서는 task 가 우선이고, OS 자원을 직접 다뤄야 하는 자리에서만 thread 로 내려가는 편이 자연스럽습니다.

3.4.2. API

기본 타입과 함수 시그니처는 아래와 같습니다.

struct Thread { /* OS handle - 제네릭 아님 */ }
struct JoinHandle<T> { /* join 결과 T */ }
struct ThreadId { /* opaque */ }

struct JoinError { code: i32; }          // OS join 에러 번호 (errno), JOIN_LOST, 또는 JOIN_TAKEN
const JOIN_LOST: i32 = -1;               // 실패한 REPL 셀과 함께 끝난 스레드
const JOIN_TAKEN: i32 = -2;              // 이미 join 또는 detach 됨
// Display, Debug, Error 를 구현

fn spawn<T: Send>(f: fn() -> T + Send) -> JoinHandle<T>;

struct Builder {
    name: str | none;
    stack_size: usize | none;
}

impl Builder {
    fn new() -> Builder;
    fn name(self, name: str) -> Builder;
    fn stack_size(self, size: usize) -> Builder;
    fn spawn<T: Send>(self, f: fn() -> T + Send) throws (IoError) -> JoinHandle<T>;
}

impl<T> JoinHandle<T> {
    fn join(self) throws (JoinError) -> T;
    fn detach(self);                     // join 하지 않음 - 스레드는 끝까지 실행
    fn thread(self) -> Thread;
}

impl Thread {
    fn id(self) -> ThreadId;
    fn name(self) -> str | none;        // Builder::name 으로 준 이름 (OS 스레드 이름으로도 설정), 없으면 none
    fn unpark(self);
}

// 현재 thread
fn current() -> Thread;
fn yield_now();
fn sleep(dur: Duration);
fn park();                                     // unpark 될 때까지 block (깨운 이유 없이 돌아올 수 있음)
fn park_timeout(dur: Duration);

// thread-local storage
fn local<T>(init: fn() -> T + Sync) -> ThreadLocal<T>;     // 각 스레드에서 처음 쓸 때 init 호출

impl<T> ThreadLocal<T> {
    fn get(self) -> T;                       // 현재 스레드의 값
    fn set(self, value: T);
    fn with<R>(self, f: fn(T) -> R) -> R;
}

Thread::unpark 는 그 스레드의 park 토큰을 세우고 깨웁니다. 토큰은 한 번의 park 가 소비합니다.

panic 과 JoinError

어느 스레드에서든 panic 이 나면 프로세스 전체가 종료 코드 101 로 끝납니다. 그래서 join 이 다른 스레드의 panic 을 받을 일은 없으며, JoinError 는 OS 수준의 join 실패, 또는 실패한 REPL 셀과 함께 끝난 스레드의 JOIN_LOST (refs-lang 9.7.0) 를 나타내며, 후자는 "thread join failed: the thread ended with a failed REPL cell" 로 표시됩니다.

JOIN_TAKEN (-2): 그 handle 이나 그 복사본이 이미 join 되었거나 detach 되었습니다. 두 번째 join 은 그것을 던지고, 두 번째 detach 는 아무것도 하지 않습니다.

3.4.3. 사용 예

가장 간단한 spawn-join 패턴부터 builder, 다중 spawn 까지 차례로 살펴봅니다.

use rok::std::thread;

let h = thread::spawn(|| {
    heavy_blocking_work()
});
let result = try h.join();

// builder
let h = try thread::Builder::new()
    .name("worker-1")
    .stack_size(4 * 1024 * 1024)
    .spawn(|| { ... });

// 다중
let handles: List<JoinHandle<i64>> = (0..10)
    .map(|i| thread::spawn(|| { compute(i) }))
    .collect();

let results: List<i64> = handles
    .into_iter()
    .map(|h| try! h.join())
    .collect();

3.4.4. thread-local storage

각 스레드마다 독립된 저장소가 필요하다면 thread::local 을 활용합니다.

let counter = thread::local(|| 0);

try thread::spawn(|| {
    counter.set(counter.get() + 1);         // 이 스레드의 값만 바뀜
}).join();

3.4.5. impl / target

플랫폼별 지원 현황은 다음과 같이 정리됩니다.

항목linux/macos/windowswasm32-wasi / 브라우저wasm32-wasi-threads
spawn✓Unsupported 를 throw✓
Builder✓Unsupported 를 throw✓
local✓✓ (스레드 하나)✓
park/unparkfutex / WaitOnAddress✓✓

wasm32-wasi 와 브라우저에는 스레드가 하나이며 thread::current().name() 은 none 입니다. wasm32-wasi-threads 에서 rok_cpu_count() 와 사용 가능한 병렬도는 4 입니다. WASI 는 코어 수를 알려 줄 수 없기 때문입니다.

GitHub에서 이 페이지 고치기