러스트 언어 입문 시리즈 - 8편: 동시성(Concurrency)과 병렬성(Parallelism)을 안전하게 다루기

이전 글에서는 러스트의 에러 처리 철학, Result와 Option, panic! 매크로를 통해 예외 없이 명시적으로 에러를 처리하는 방식을 살펴보았습니다. 이제는 러스트가 무엇보다 강력하게 내세우는 장점 중 하나인 동시성(Concurrency)병렬성(Parallelism) 지원에 대해 알아보겠습니다.

 

C++도 C++11 이후 std::thread, std::mutex, std::atomic 등을 통해 멀티스레딩을 지원하지만, 여전히 개발자가 락(Lock) 관리나 데이터 경쟁(Race Condition), 댕글링 포인터 문제에 신경 써야 합니다. 반면 러스트는 언어 차원에서 안전성 보장을 강화하여, 고성능 병렬 코드를 작성하면서도 메모리 안전성과 데이터 경쟁 방지를 지원합니다.

기본 스레드 사용하기

러스트에서 스레드는 std::thread 모듈을 통해 생성할 수 있습니다. C++에서 std::thread를 통해 스레드를 시작하는 것과 유사합니다.

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..5 {
            println!("새 스레드: {}", i);
            thread::sleep(Duration::from_millis(500));
        }
    });

    for i in 1..5 {
        println!("메인 스레드: {}", i);
        thread::sleep(Duration::from_millis(500));
    }

    handle.join().unwrap();
}

thread::spawn으로 새로운 스레드를 생성하고, join()으로 해당 스레드가 종료될 때까지 기다립니다. C++와 유사하지만, 러스트는 클로저를 통해 스레드 본체를 전달하며, 클로저 캡처 시에도 소유권, 빌림 규칙을 적용해 안전성을 확보합니다.

메시지 패싱(Message Passing)과 채널(Channel)

C++에서 스레드 간 통신을 위해서는 공용 변수에 락을 걸거나, std::condition_variable 등을 사용해야 합니다. 반면 러스트는 언어 철학으로 "상태를 공유하기보다 메시지를 통해 소통하라!"를 강조하며, 표준 라이브러리에 채널(Channel)을 제공합니다.

use std::sync::mpsc; // multi-producer, single-consumer
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let msg = String::from("안녕, 러스트!");
        tx.send(msg).unwrap();
        // msg는 여기서 더 이상 사용할 수 없음(소유권이 이동됨)
    });

    let received = rx.recv().unwrap();
    println!("수신한 메시지: {}", received);
}

mpsc::channel은 송신자(tx)와 수신자(rx)를 반환합니다. 한 스레드에서 tx.send()로 메시지를 보내고, 다른 스레드에서 rx.recv()로 메시지를 받습니다. 여기서 메시지는 소유권을 가진 값이 이동(move)되므로, 데이터 경쟁 없이 안전하게 스레드 간 통신이 가능해집니다.

 

C++에서는 이런 메시지 패싱을 위해 std::channel이 없으므로 외부 라이브러리를 쓰거나, 큐와 조건 변수, 뮤텍스 조합으로 직접 구현해야 합니다. 러스트는 언어 차원에서 안전한 메시지 패싱을 지원합니다.

공유 메모리 동시성과 Arc, Mutex

메시지 패싱 외에도 공유 메모리를 통한 스레드 간 협업이 필요할 때가 있습니다. 러스트는 Arc(Atomic Reference Counted smart pointer)와 Mutex를 통해 안전한 공유 메모리 접근을 지원합니다.

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

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

    for _ in 0..10 {
        let c = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = c.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    for h in handles {
        h.join().unwrap();
    }

    println!("결과: {}", *counter.lock().unwrap());
}
  • Arc: C++의 std::shared_ptr와 유사하지만, 원자적 참조 카운팅을 통해 스레드 안전성을 보장합니다.
  • Mutex: 상호 배제 락으로, C++ std::mutex와 유사합니다. 하지만 러스트의 타입 시스템은 Mutex<T>를 통해 "이 자료구조에 접근할 때 반드시 lock을 거쳐야 한다"는 점을 강제하고, lock()의 반환값이 MutexGuard로 관리되어 스코프를 벗어날 때 자동으로 락이 해제됩니다.

이로써 C++에서 무심코 raw 포인터나 mutex lock/unlock 관리를 잘못하는 일 없이, 컴파일러가 타입 시스템을 통해 많은 오류를 사전에 차단해줍니다.

Send와 Sync 트레이트

러스트는 어떤 타입이 스레드 간에 안전하게 이동(Transfer)하거나 공유될 수 있는지를 두 가지 트레이트로 표현합니다.

  • Send 트레이트: 타입이 다른 스레드로 이동해도 안전함을 의미합니다.
  • Sync 트레이트: 타입이 여러 스레드 간에 동시에 참조해도 안전함을 의미합니다.

C++에서는 이런 개념을 문서나 주석으로 표현하거나, 개발자가 직접 원자적 타입을 사용해 스레드 안전성을 보장해야 합니다. 반면 러스트에서는 기본 타입 대부분이 Send와 Sync를 자동 구현하며, 개발자가 만든 타입도 이 조건을 만족하지 못하면 스레드 간 공유 시 컴파일 에러를 유발합니다. 이로써 스레드 안전성을 언어 차원에서 검증할 수 있습니다.

고수준 동시성: Rayon, Tokio 등

표준 라이브러리의 기본 기능 외에도, 러스트 생태계에는 고수준의 동시성 라이브러리가 풍부합니다.

  • Rayon: 데이터 병렬성을 간단히 표현할 수 있는 라이브러리. iter().par_iter()로 손쉽게 멀티코어 활용을 극대화할 수 있습니다.
  • Tokio: 비동기 런타임으로, async/await를 통해 비동기 IO 작업을 효율적으로 처리할 수 있습니다. C++의 asio 라이브러리나 Boost.Asio와 유사한 목적을 갖고 있지만, async/await 문법을 통해 더 명확하고 안정적인 비동기 코드를 작성할 수 있습니다.

이는 C++20 코루틴과 비교할 만한데, 러스트는 async/await를 언어 차원에서 깔끔히 지원하고, 다양한 고성능 비동기 런타임을 제공합니다.

C++와의 비교 정리

  • 스레드 생성: C++11 std::thread와 유사. 러스트는 클로저로 스레드 함수를 전달.
  • 메시지 패싱: 표준 라이브러리에 안전한 채널 제공. C++은 별도 구현 필요.
  • 공유 메모리: Arc<Mutex<T>>를 통한 안전한 공유. C++ std::shared_ptr<std::mutex> 조합보다 철저히 타입 안정성이 보장.
  • 스레드 안전성 보장 메커니즘: Send와 Sync 트레이트로 컴파일 타임 검증. C++은 개발자 주의에 의존.
  • 비동기: Tokio 등 강력한 런타임. C++20 코루틴과 비슷한 목적이나, 러스트는 async/await와 런타임 생태계가 더 통합적.

앞으로의 학습 방향

이번 글에서는 러스트가 제공하는 동시성과 병렬성 지원에 대해 C++와 비교해보았습니다. 다음 글에서는 러스트의 메타프로그래밍 격인 매크로(Macro) 시스템빌드 스크립트, 클로저(Closure), 함수형 패러다임 지원 등에 대해 살펴보며 러스트 생태계의 풍부한 표현력을 익혀보겠습니다.

유용한 링크와 리소스

반응형