Thread

use std::thread;
fn main() {
    thread::spawn(||{
        for i in 1..=9{ print!("a{i} "); }
    });
}
  • std::thread::spawn(|| ...);와 같은 형태로 사용합니다.
  • 클로저 함수에 쓰레드로 동작시킬 루틴을 넣으면 됩니다.


sleep

thread::sleep(Duration::from_millis(100));
  • thread::sleep(dur)를 이용해서 잠시 멈추게 할 수 있습니다.
  • Duration::from_millis(1) 같은 duration을 이용해 시간을 정할 수 있습니다.


join

use std::thread;
fn main() {
    let handle = thread::spawn(||{
        for i in 1..=9{ print!("a{i} "); }
    });

    let _ = handle.join();
}
  • 쓰레드가 끝날 때까지 기다립니다.


use std::thread;

fn main() {
    let handle = thread::spawn(|| {
        let mut sum = 0;
        for i in 1..=100 { sum += i; }
        sum //이 값이 join으로 전달
    });

    let result = handle.join().unwrap();
    println!("sum = {}", result);   //sum = 5050
}
  • 쓰레드의 리턴 값을 가져올 수도 있습니다.



쓰레드간 데이터 '전송'

  1. 메인에서 생성된 변수를 쓰레드의 클로저에서 그냥 사용
    • let handle = thread::spawn(move || {  // move 사용
          println!("v: {:?}", v);
          // v는 main에서 정의된 변수로, move에 의해 소유권이 이동되었음
      });
      
    • spawn에서 클로저를 생성할 때 move 키워드를 써야 합니다. (소유권 강제 이동)
    • 메인에서의 데이터를 쓰레드에서 '참조' 형태로 borrow해서 사용하는 것을 허락하지 않습니다.


  1. mpsc (multi-producer, single-consumer) 채널 사용
    • let (tx, rx) = mpsc::channel();
      let tx1 = tx.clone();
      let handle1 = thread::spawn (move||{
          ...
          tx1.send(val).unwrap(); // 송신. val 소유권 이동
      });
      
      let handle2 = thread::spawn (move||{
          ...
          tx.send(val).unwrap(); // 송신. val 소유권 이동
      });
      
      let sum = rx.recv().unwrap(); // 수신.
      
      • mpsc::channel()에 의해 리턴된 Sendersend(data) 메서드를 이용합니다.
      • 채널은 일종의 FIFO(First-in First-out) 버퍼
    • for r in rx {
          println!("{}", r);
      }
      
      • 이런 식으로 코드를 작성해서 버퍼 메시지를 모두 읽을 수 있습니다. 신기하게도 버퍼가 비어있다고 끝나지 않습니다.
      • 루프가 끝나는 조건은 "모든 Sender가 사라졌을 때"입니다.
      • rx.recv(): 가장 오래된 메시지 하나 읽음


  1. mpmc (multi-producer, multi-consumer) 채널 사용
    • 아직 안정화가 되어있지 않습니다. (2026.9월 기준)
    • nightly 전용 unstable 기능이라고 합니다.



쓰레드간 데이터 '공유'


단일 쓰레드멀티 쓰레드
공유만Rc<T>Arc<T>
공유 + 수정Rc<RefCell<T>>Arc<Mutex<T>>


Rc (Reference counted)

  • 레퍼런싱하는 메모리에의 접근을 카운터를 써서 관리합니다.
  • 여러 개의 변수들이 동일한 힙 메모리 영역을 참조하며 사용할 수 있습니다.
use std::rc::Rc;

fn main() {
    let s1 = Rc::new(String::from("hello"));
    println!("{}", Rc::strong_count(&s1)); //1

    let s2 = Rc::clone(&s1);
    println!("{}", Rc::strong_count(&s1)); //2

    {
        let s3 = Rc::clone(&s1);
        println!("{}", Rc::strong_count(&s1)); //3
    }   //여기서 s3가 사라지면서 카운트가 줄어든다

    println!("{}", Rc::strong_count(&s1)); //2
    println!("{} {}", s1, s2);
}
  • Rc<T>로 감싼 값은 읽을 수만 있고 고칠 수 없습니다.
  • Rc::clone(): Rc 객체가 추가로 복제되는 것이 아니고, 단지 Rc 내의 'Ref count'의 값만 증가합니다.


Arc (Atomic Reference Counted)

  • Rc와 마찬가지로 힙 메모리 공간을 여러 변수가 참조할 수 있습니다.
  • Rc와의 차이는 참조 카운트를 원자적으로 세느냐 아니냐 입니다.
  •     let mut handles = vec![];
        for i in 0..=2 {
            let ss = Arc::clone(&s);
            handles.push(thread::spawn(move || {
      		  println!("{}: {:?}", i, ss);
      	  }));
        }
        for h in handles { h.join().unwrap(); }
    


Mutex (Mutual Exclusion)

use std::sync::Mutex;
fn main() {
    let m = Mutex::new(5);
    {
        let mut n = m.lock().unwrap();
        *n = 6;
    }
    println!("m = {:?}", m);   //m = Mutex { data: 6, poisoned: false, .. }
}
  • std::sync::Mutex를 통해 뮤텍스를 사용할 수 있습니다.
  • Rust의 Mutex에는 unlock() 메서드가 없습니다. 좀 특이합니다.
    • 락이 풀리는 시점은 MutexGuardDrop될 때 (즉 스코프를 벗어날 때)
    • 락을 일찍 풀고 싶을 때는 스코프를 좁히거나 drop(n);을 명시적으로 호출하면 됩니다.
  • m.lock() 뒤에 .unwrap()가 필요한가?
    • 락을 잡고 있던 쓰레드가 패닉에 빠지는 경우 락을 잡는 걸 실패할 수 있다.
  • 출력에 poisoned: false란 값이 있는데 이는 오염된 데이터인지 알려줍니다.


Arc + Mutex 예제

use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

fn main(){
    let m = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for i in 0..10 {
        let mm = Arc::clone(&m); 
        let handle = thread::spawn(move || {
            let mut n = mm.lock().unwrap(); // <- lock
            thread::sleep(Duration::from_millis(1));
            
            println!("Thread {}: Before increasing. num = {}", i, n);
            *n += 1;
            println!("Thread {}: After increasing. num = {}", i, n);
            
        });
        handles.push(handle);
    }

    println!("{:?}", Arc::strong_count(&m));
    handles.into_iter().for_each(|h| h.join().unwrap());
    println!("Result: {}", *m.lock().unwrap());
}




비동기 프로그래밍


Rust에서는 비동기 프로그래밍을 지원하는 Future 트레잇과 async/await 기능이 있습니다. Green 쓰레드의 실행과 스케줄링을 하는 기능은 없어서 외부 라이브러리를 사용해야 합니다.

  • tokio: 2017년부터 소개된 Rust의 대표적인 비동기 프로그래밍 라이브러리
  • smol: 가볍고 단순한 구조를 지향하는 런타임. tokio보다 작고 명시적


tokio

$cargo add tokio --features full
  • 프로젝트로 이동 후 tokio를 add 합니다.
[dependencies] tokio = { version = "1", features = ["full"] }
  • Cargo.toml 파일에 다음과 같이 추가됩니다.


async fn say_hello(){
    println!("hello");
}

#[tokio::main]
async fn main() {
    say_hello().await;
}
  • main 함수에 대해 async로 지정하기 위해서는 #[tokio::main] 매크로를 지정해줘야 합니다.
  • .await를 사용해야 실행이 됩니다.
    • 자바스크립트에서는 await가 없어도 async 함수 호출이 바로 되지만 Rust에서는 그렇지 않습니다.
    • (다른 언어와 사용법이 조금 달라 헷갈릴 수 있습니다.)


use tokio::time::{sleep, Duration};

async fn work(name: &str, ms: u64) {
    sleep(Duration::from_millis(ms)).await;
    println!("{} 완료", name);
}

#[tokio::main]
async fn main() {
    //(1) 순차 실행: 총 300ms가 걸린다
    work("A", 100).await;
    work("B", 200).await;

    //(2) 동시 실행: 총 200ms면 끝난다
    let h1 = tokio::spawn(work("C", 100));
    let h2 = tokio::spawn(work("D", 200));
    h1.await.unwrap();
    h2.await.unwrap();
}
  • tokio::spawn으로 각각을 별도의 작업(task)으로 띄울 수 있습니다.


#[tokio::main]
async fn main() {
    // 여기까지는 Core 쓰레드에서 수행되는 영역이다. 

    let blocking_task = tokio::task::spawn_blocking(|| {
        // 여기에 블로킹되어 수행되는 코드를 넣으면 된다. 
    });

    // Blocking 쓰레드가 생성되고, blocking_task가 수행 대기 상태로 된다.  
    blocking_task.await.unwrap();
}
  • Core 쓰레드는 여러 작업이 나눠 쓰는 쓰레드입니다. 그래서 어떤 작업이 여기서 오래 붙잡고 있으면 다른 작업들이 전부 멈춥니다.
    • CPU를 오래 쓰는 계산
    • std::thread::sleep처럼 쓰레드를 재우는 함수
    • tokio 버전이 없는 라이브러리의 블로킹 함수
  • 이런 것들을 spawn_blocking으로 감싸면 별도의 OS 쓰레드로 넘어가서 Core 쓰레드를 막지 않습니다.
    • tokio::fs, tokio::net 같은 비동기 버전이 있는 작업은 spawn_blocking을 쓸 필요 없이 그냥 .await하면 됩니다!


주의할 점

std::thread::sleep을 쓰면 안됩니다.

  • std::thread::sleep : OS 쓰레드 자체를 재웁니다. 그 쓰레드에서 돌던 다른 비동기 작업들까지 전부 함께 멈춥니다.
  • tokio::time::sleep : 그 작업만 잠재우고 런타임은 같은 쓰레드에서 다른 작업을 계속 처리합니다.


비동기 파일 읽기/쓰기

하는 일동기 (std)비동기 (tokio)
전체 읽기fs::read_to_stringtokio::fs::read_to_string
전체 쓰기fs::writetokio::fs::write
파일 열기File::opentokio::fs::File::open
조금씩 읽기Read 트레잇AsyncReadExt 트레잇
버퍼 사용BufReadertokio::io::BufReader


읽기 예제 1

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>>{
    let txt = tokio::fs::read_to_string("hello.txt").await?;    
    println!("File contents: \n{}", txt);
    Ok(())
}


읽기 예제 2

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let contents = tokio::fs::read("hello.txt").await?;
    let txt = String::from_utf8(contents)?;
    println!("File contents: \n{}", txt);
    Ok(())
}
  • read는 파일 전체 내용을 읽어서 Result<Vec<u8>>을 리턴합니다.


쓰기 예제

use tokio::fs;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>>{
    fs::write("tmp.txt", b"Hello world!").await?;
    Ok(())
}
  • 파일이 이미 있으면 기존 내용을 모두 지우고 새로 씁니다.
    • 뒤에 이어서 쓸려면 OpenOptions을 써야합니다.
  • 한글을 쓰려면 b"..." 대신 "헬로 러스트".as_bytes()를 써야 합니다.
    • 바이트 문자열 리터럴(b"...")은 아스키만 담을 수 있기 때문입니다.