thread -- 스레딩

SafeC는 두 개의 계층으로 이루어진 스레딩을 제공합니다: spawn/join/spawn_scoped 언어 키워드(컴파일러 내장 기능으로, #include가 필요 없습니다)와, 뮤텍스/조건 변수/읽기-쓰기 락과 함께 OS 스레드를 직접 제어할 수 있는 아래의 std::thread_* 라이브러리 API(thread.h)입니다. 두 계층 모두 Linux/macOS에서는 POSIX pthreads, Windows에서는 Win32 스레드를 기반으로 동작합니다. 프리스탠딩/베어메탈 타깃(또는 컴파일러가 인식하지 못하는 모든 타깃)에서는 언어 키워드가 대신 문서화된 두 개의 심볼 훅으로 컴파일되며, std/sync/bare_spawn.sc가 이를 협조적으로 구현합니다. 해당 백엔드와, 베어메탈에 친화적인 std::thread_create/join의 사촌 격 함수들에 대해서는 동기화를 참고하십시오.

언어 레벨 spawn / join / spawn_scoped

c
long long spawn(fn, arg);          // fn: &static 함수 참조; 스레드 핸들을 반환
void      join(handle);
long long spawn_scoped(fn, arg);   // spawn과 같지만, 시작한 함수가 반환하기 전에 join됨

spawn의 첫 번째 인자는 함수에 대한 &static 참조여야 합니다(일반적인 최상위 함수 이름은 암묵적으로 이 형태로 붕괴합니다). 두 번째 인자는 스레드의 인자로 그대로 전달됩니다. 스레드 함수는 nullable 참조를 받고 반환합니다(?&T f(?&T arg)). 인자는 검사됩니다(스레드 안전성 참고): 생성된 스레드가 참조 대상보다 오래 살거나 그 위에서 경쟁할 수 있으므로 spawn의 인자는 null이거나 정적 데이터를 가리켜야 하며, spawn_scoped는 지역 변수를 빌려줄 수 있습니다. spawn과 spawn_scoped 모두 join에 전달할 long long 핸들을 반환합니다.

c
extern int printf(const char *fmt, ...);

static ?&int worker0(?&int unused) { unsafe { printf("thread 0\n"); } return null; }
static ?&int worker1(?&int unused) { unsafe { printf("thread 1\n"); } return null; }

int main() {
    long long h0 = spawn(worker0, null);
    long long h1 = spawn(worker1, null);

    join(h0);
    join(h1);

    printf("all threads done\n");
    return 0;
}
플레이그라운드에서 실행

리전 격리

spawn으로 시작한 스레드는 어떤 스코프에서도 join되지 않으므로, 그 인자는 null이거나 정적 데이터를 가리켜야 합니다: T가 Sync인 const &static T, 또는 T가 자기 상태를 스스로 동기화할 때(Mutex, 원자 연산)만 가변 &static T. 스택, 아레나, 힙 데이터를 가리키는 참조는 컴파일 오류이며("the argument '&stack int' refers to data the thread may outlive"), 원시 포인터는 unsafe 안에서만 허용됩니다. 스레드에 지역 변수를 넘기려면 spawn_scoped를 쓰세요. 전체 규칙은 스레드 안전성에 있습니다.

백엔드 선택

컴파일러는 빌드 타깃에 따라 spawn/join/spawn_scoped의 백엔드를 선택합니다.

타깃 백엔드
--freestanding 훅(아래 참고) — OS 스레드 API를 전혀 가정하지 않음
Windows를 지정하는 명시적 --target Win32 (CreateThread/WaitForSingleObject)
Linux/macOS/*BSD/Solaris를 지정하는 명시적 --target Pthreads
그 외를 지정하는 명시적 --target (예: wasm32-*, riscv64-unknown-elf) 훅
--target 미지정 Pthreads (호스트 기본값)

훅 백엔드는 spawn/join을 두 개의 고정된 extern 심볼 — __safec_thread_create(func, arg) -> i64와 __safec_thread_join(i64) -> void — 에 대한 호출로 컴파일합니다. 이는 의도적인 확장 지점으로, 정확히 이 두 심볼을 제공하는 런타임이라면 컴파일러 변경 없이 spawn/join을 지원할 수 있습니다. std/sync/bare_spawn.sc는 스폰된 함수를 std::TaskScheduler 위에서 협조적으로 실행하는 참조 구현입니다(동기화 참고). 벤더 RTOS 셰임(shim)이 동일한 두 심볼을 대신 정의할 수도 있습니다.

타입 있는 스레드와 락

아래의 핸들 기반 함수 위에 만든 std/thread.h의 검사되는 계층입니다. 컴파일러의 Send/Sync 및 소유권 검사가 여기에 적용됩니다: thread_spawn으로 옮긴 값은 그 뒤에 쓸 수 없고, drop하지 않은 가드는 누수 오류이며, get()이 준 참조는 가드보다 오래 살 수 없습니다.

c
#include <std/thread.h>

// A thread that owns its argument (T: Send, R: Send). A Drop value.
generic<T, R> struct SpawnedThread {
    int  started() const;           // 0 if the thread couldn't be created
    R    join() consume;            // waits; f's result
    void drop();                    // join() and drop the result
};
generic<T: Send, R: Send> struct SpawnedThread<T, R> thread_spawn(fn R(T) f, T arg);   // arg moves in

generic<T: Send> struct Mutex {     // Sync: share it between threads
    static struct Mutex<T> new(T value);
    struct MutexGuard<T> lock() const;   // blocks; the guard is the only way to the value
    T    into_inner() consume;      // the value back
    void drop();
};
generic<T> struct MutexGuard {      // !Send: unlock on the locking thread
    &T   get();                     // valid until drop()
    void set(T value);              // replace (dropping the old value)
    void drop();                    // unlocks
};

generic<T: Send + Sync> struct RwLock {
    static struct RwLock<T> new(T value);
    struct RwReadGuard<T>  read() const;    // many at once: get() is const &T
    struct RwWriteGuard<T> write() const;   // exclusive: get() is &T
    void drop();
};

Mutex와 RwLock은 값을 힙 상자에 두며, 락을 건 뒤에는 옮기면 안 됩니다(가드가 이를 가리킵니다). 공유하는 것은 static에 두고 스레드를 시작하기 전에 초기화하십시오. 한 스레드에서 두 번 락을 걸면 (POSIX에서) 교착 상태가 됩니다 — OS 수준 뮤텍스 참고.

c
#include <std/thread.h>
#include <std/collections/vec.h>
extern int printf(const char *fmt, ...);

static struct Mutex<int> g_total;

long long sum(struct Vec<int> v) {           // owns v: nothing is shared
    long long s = 0LL;
    for (unsigned long i = 0UL; i < v.len; i = i + 1UL) s = s + (long long)*v.at(i);
    v.drop();
    return s;
}

?&int add_ten(?&int unused) {
    for (int i = 0; i < 10; i = i + 1) {
        struct MutexGuard<int> g = g_total.lock();
        *g.get() = *g.get() + 1;
        g.drop();                            // unlocks
    }
    return null;
}

int main() {
    struct Vec<int> v = Vec::new();
    for (int i = 1; i <= 100; i = i + 1) v.push(i);
    struct SpawnedThread<struct Vec<int>, long long> t = thread_spawn(sum, v);   // v moves in
    long long s = t.join();

    g_total = Mutex::new(0);
    long long h1 = spawn(add_ten, null);
    long long h2 = spawn(add_ten, null);
    join(h1);
    join(h2);
    int total = g_total.into_inner();

    struct RwLock<int> limit = RwLock::new(5);
    struct RwReadGuard<int> r = limit.read();
    int seen = *r.get();
    r.drop();
    struct RwWriteGuard<int> w = limit.write();
    *w.get() = 6;
    w.drop();
    limit.drop();

    printf("%lld %d %d\n", s, total, seen);
    return 0;
}

직접 OS 스레드 API (thread.h)

이 페이지의 나머지 부분 — thread_create/mutex_*/cond_*/rwlock_* — 는 위의 spawn/join 키워드 대신 OS 스레드를 직접 제어하고 싶을 때 사용하는, 별도로 명시적으로 호출하는 API입니다. 두 계층 모두 호스티드 타깃에서는 pthread/Win32 기반으로 동작합니다. thread_create의 핸들은 Win32에서 spawn의 핸들과 ABI 호환이지만, 그 외에는 두 API가 서로 독립적입니다(이 API는 훅 백엔드 계약의 일부가 아닙니다).

검사되는 스레드

std/thread.h에는 컴파일러의 Send/Sync 검사가 적용되는 타입 있는 계층도 있습니다: thread_spawn(f, arg)는 인자(T: Send)를 새 스레드로 옮기고 SpawnedThread<T, R>를 반환하며, 그 join()이 f의 결과를 돌려줍니다. Mutex<T> / RwLock<T>는 보호하는 값을 소유합니다. 아래의 핸들 기반 함수는 스레드가 무엇을 공유하는지 전혀 검사하지 않으므로 타입 있는 계층을 쓰세요.

c
#include <std/thread.h>

핸들 표현

모든 핸들(스레드 ID, 뮤텍스, 조건 변수, 읽기-쓰기 락)은 unsigned long long으로 저장됩니다.

  • POSIX: pthread_t 값(64비트 환경에서 8바이트), 또는 힙에 할당된 pthread 객체를 가리키는 포인터
  • Win32: HANDLE(커널 객체 포인터, x64에서 8바이트), 또는 힙에 할당된 Windows 프리미티브를 가리키는 포인터

스레드

c
generic<T>
int  thread_create(&unsigned long long tid, fn ?&T(?&T) func, ?&T arg);
int  thread_join(unsigned long long tid);
int  thread_detach(unsigned long long tid);
void thread_yield();
void thread_sleep_ms(unsigned long ms);
unsigned long long thread_self();

thread_create

새 스레드에서 func(arg)를 시작하고 그 핸들을 *tid에 기록합니다. 성공 시 0을 반환합니다. 스레드 함수는 자신의 컨텍스트 타입에 대한 nullable 참조를 받고 반환하며 — static ?&Job work(?&Job job) — T는 이로부터 추론되므로, 잘못된 컨텍스트 타입을 넘기면 컴파일 오류입니다. 스레드에 인자가 필요 없으면 null을 넘깁니다. 반환값은 버려집니다(thread_join은 기다리기만 합니다).

그 아래의 타입 없는 OS 진입점(POSIX void*(*)(void*), Win32 LPTHREAD_START_ROUTINE)은 thread_create_raw_입니다. 기계 수준에서 ?&T는 평범한 포인터이므로 ABI가 동일합니다.

thread_join

스레드 tid가 종료될 때까지 블록합니다. 성공 시 0을 반환합니다.

thread_detach

스레드 tid를 분리(detach)하여 종료 시 리소스가 자동으로 회수되도록 합니다. 성공 시 0을 반환합니다.

thread_yield

현재 스레드의 타임 슬라이스를 다른 실행 가능한 스레드에게 양보합니다.

thread_sleep_ms

호출한 스레드를 ms 밀리초 동안 재웁니다.

thread_self

호출한 스레드의 ID를 반환합니다.

뮤텍스

c
int mutex_init(&unsigned long long m);
int mutex_destroy(&unsigned long long m);
int mutex_lock(&unsigned long long m);
int mutex_trylock(&unsigned long long m);
int mutex_unlock(&unsigned long long m);

mutex_init / mutex_destroy

뮤텍스를 초기화하거나 파괴합니다. 뮤텍스 핸들은 *m에 저장됩니다.

mutex_lock / mutex_unlock

뮤텍스를 획득하거나 해제합니다. mutex_lock은 다른 스레드가 뮤텍스를 보유하고 있으면 블록합니다.

mutex_trylock

블록하지 않고 뮤텍스 획득을 시도합니다. 락을 획득했으면 0을, 사용 중이면 0이 아닌 값을 반환합니다.

호출 스레드가 이미 보유한 뮤텍스를 다시 잠그면 안 됩니다: Windows(CRITICAL_SECTION)에서는 성공하지만, 기본 POSIX 뮤텍스는 mutex_lock에서 교착 상태가 되고 mutex_trylock에서 사용 중을 보고합니다.

조건 변수

c
int cond_init(&unsigned long long cv);
int cond_destroy(&unsigned long long cv);
int cond_wait(&unsigned long long cv, &unsigned long long m);
int cond_timedwait_ms(&unsigned long long cv, &unsigned long long m, unsigned long ms);
int cond_signal(&unsigned long long cv);
int cond_broadcast(&unsigned long long cv);

cond_wait

뮤텍스 *m을 원자적으로 해제하고 조건 변수가 시그널될 때까지 블록합니다. 반환하기 전에 뮤텍스를 다시 획득합니다.

cond_timedwait_ms

cond_wait와 같지만 최대 ms 밀리초만 기다립니다. 깨어나면 0, 시간이 다 되면 THREAD_TIMEDOUT(1), 그 밖의 오류는 -1을 반환하며, 모든 플랫폼에서 같은 값입니다.

cond_signal / cond_broadcast

조건 변수를 기다리는 스레드 중 하나(signal) 또는 전체(broadcast)를 깨웁니다.

읽기-쓰기 락

c
int rwlock_init(&unsigned long long rw);
int rwlock_destroy(&unsigned long long rw);
int rwlock_rdlock(&unsigned long long rw);
int rwlock_wrlock(&unsigned long long rw);
int rwlock_tryrdlock(&unsigned long long rw);
int rwlock_trywrlock(&unsigned long long rw);
int rwlock_rdunlock(&unsigned long long rw);
int rwlock_wrunlock(&unsigned long long rw);

여러 개의 리더가 동시에 접근하는 것은 허용되지만, 라이터는 배타적으로 접근합니다. 공유 읽기 접근에는 rdlock/rdunlock을, 배타적 쓰기 접근에는 wrlock/wrunlock을 사용합니다.

예제

c
#include <std/thread.h>
#include <std/io.h>
#include <std/atomic.h>

static int counter = 0;
static unsigned long long mtx = 0ULL;

static ?&int worker(?&int unused) {
    int i = 0;
    while (i < 1000) {
        std::mutex_lock(&mtx);
        counter = counter + 1;
        std::mutex_unlock(&mtx);
        i = i + 1;
    }
    return null;
}

int main() {
    std::mutex_init(&mtx);

    unsigned long long t1 = 0ULL;
    unsigned long long t2 = 0ULL;
    std::thread_create(&t1, worker, null);
    std::thread_create(&t2, worker, null);

    std::thread_join(t1);
    std::thread_join(t2);

    std::print("counter = ");
    std::println_int(counter);  // 2000

    std::mutex_destroy(&mtx);
    return 0;
}

생산자-소비자 예제

c
#include <std/thread.h>
#include <std/io.h>

static int buffer = 0;
static int ready  = 0;
static unsigned long long mtx = 0ULL;
static unsigned long long cv = 0ULL;

static ?&int producer(?&int unused) {
    std::mutex_lock(&mtx);
    buffer = 42;
    ready  = 1;
    std::cond_signal(&cv);
    std::mutex_unlock(&mtx);
    return null;
}

static ?&int consumer(?&int unused) {
    std::mutex_lock(&mtx);
    while (ready == 0) {
        std::cond_wait(&cv, &mtx);
    }
    std::print("received: ");
    std::println_int(buffer);  // 42
    std::mutex_unlock(&mtx);
    return null;
}

int main() {
    std::mutex_init(&mtx);
    std::cond_init(&cv);

    unsigned long long t1 = 0ULL;
    unsigned long long t2 = 0ULL;
    std::thread_create(&t1, consumer, null);
    std::thread_create(&t2, producer, null);

    std::thread_join(t1);
    std::thread_join(t2);

    std::cond_destroy(&cv);
    std::mutex_destroy(&mtx);
    return 0;
}