본문 바로가기
IT/Rust 기초 완전 정복

Rust 동시성 프로그래밍 기초 (17)

by 지식 발전소 2024. 4. 24.
728x90
반응형

1

 

스레드(Threads)의 이해

이번에는 Rust에서 동시성 프로그래밍의 기초인 스레드(Threads) 에 대해 알아보겠습니다. 스레드란 무엇일까요? 스레드는 프로그램 내에서 실행되는 작업의 단위입니다. 하나의 프로세스 내에서 여러 개의 스레드가 동시에 실행될 수 있어, 프로그램의 성능과 반응성을 높일 수 있습니다.

 

왜 스레드를 사용해야 할까요? 예를 들어, 대규모 데이터 처리나 네트워크 통신과 같은 작업을 수행할 때, 단일 스레드로는 처리 속도가 느려질 수 있습니다. 이때 작업을 여러 스레드로 나누어 병렬적으로 처리하면 전체 실행 시간을 단축할 수 있습니다. 또한, 스레드를 사용하면 GUI 애플리케이션에서 메인 스레드와 별도의 스레드를 사용하여 사용자 인터페이스의 반응성을 높일 수 있습니다.

 

Rust에서는 std::thread 모듈을 통해 스레드를 생성하고 관리할 수 있습니다. 아래 예시를 통해 스레드 생성 및 실행 방법을 알아보겠습니다.

use std::thread;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("스레드 데이터: {}", i);
        }
    });

    for i in 1..5 {
        println!("메인 스레드 데이터: {}", i);
    }

    handle.join().unwrap();
}

 

위 예시에서 thread::spawn 함수를 사용하여 새로운 스레드를 생성합니다. 이때 클로저(closure)를 전달하여 해당 스레드에서 실행할 작업을 정의합니다. 메인 스레드와 새로 생성된 스레드는 각각 별도의 작업을 병렬적으로 실행합니다. handle.join().unwrap()은 생성된 스레드가 작업을 완료할 때까지 대기합니다.

 

스레드를 사용할 때는 데이터 레이스(Data Race)  교착 상태(Deadlock) 와 같은 문제에 주의해야 합니다. 데이터 레이스는 여러 스레드가 동시에 같은 메모리 영역에 접근하여 발생하는 문제이며, 교착 상태는 여러 스레드가 서로를 기다리는 상황에서 발생합니다. Rust는 이러한 문제를 방지하기 위해 소유권(Ownership)  참조 차용(Borrowing) 규칙을 엄격하게 적용하여 스레드 안전성을 보장합니다.

메시지 전달(Message Passing)

동시성 프로그래밍에서 스레드 간 통신은 매우 중요합니다. Rust에서는 메시지 전달(Message Passing) 방식을 통해 스레드 간 데이터를 안전하게 주고받을 수 있습니다. 이를 위해 std::sync::mpsc 모듈에 제공되는 다중 생산자, 단일 소비자 채널(Multiple Producer, Single Consumer Channel) 을 사용합니다.

 

채널은 mpsc::channel 함수를 통해 생성됩니다. 이 함수는 (송신 엔드포인트, 수신 엔드포인트) 튜플을 반환합니다. 송신 엔드포인트를 통해 데이터를 전송하고, 수신 엔드포인트에서 해당 데이터를 받을 수 있습니다.

use std::sync::mpsc;
use std::thread;

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

    let handle = thread::spawn(move || {
        let data = vec![1, 2, 3, 4, 5];
        for val in data {
            tx.send(val).unwrap();
            println!("송신 데이터: {}", val);
        }
    });

    for received in rx {
        println!("수신 데이터: {}", received);
    }

    handle.join().unwrap();
}

 

위 예시에서 mpsc::channel()을 사용하여 채널을 생성합니다. 새로운 스레드에서 tx 송신 엔드포인트를 통해 데이터를 전송하고, 메인 스레드에서 rx 수신 엔드포인트로 데이터를 받습니다. for received in rx 구문은 수신 엔드포인트를 반복하여 전달된 모든 데이터를 처리합니다.

 

메시지 전달 방식을 사용하면 스레드 간 데이터를 안전하게 주고받을 수 있습니다. 또한, 채널의 버퍼링 기능을 활용하여 성능을 최적화할 수 있습니다. 다만, 송신 엔드포인트가 모두 드롭(drop)된 후에는 수신 엔드포인트가 더 이상 데이터를 받을 수 없다는 점에 주의해야 합니다.

공유 상태(Shared State)

동시성 프로그래밍에서 여러 스레드가 데이터를 공유하는 경우가 자주 있습니다. Rust에서는 뮤텍스(Mutex)  원자적 참조 카운팅(Arc) 을 통해 안전한 공유 상태를 구현할 수 있습니다.

뮤텍스(Mutex)

뮤텍스(Mutex) 는 여러 스레드가 동시에 데이터에 접근하지 못하도록 상호 배제(mutual exclusion)를 보장하는 동기화 프리미티브(synchronization primitive)입니다. 한 번에 하나의 스레드만 뮤텍스 가드(guard)를 획득하여 데이터에 접근할 수 있습니다.

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 counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut value = counter.lock().unwrap();
            *value += 1;
        });
        handles.push(handle);
    }

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

    println!("결과: {}", *counter.lock().unwrap());
}

 

위 예시에서 Arc<Mutex<T>>를 사용하여 스레드 안전한 공유 데이터를 생성합니다. Arc는 여러 스레드에서 참조 카운트를 공유할 수 있도록 해주며, Mutex는 여러 스레드가 동시에 데이터에 접근하지 못하도록 보장합니다.

 

각 스레드에서 counter.lock().unwrap()을 통해 뮤텍스 가드를 획득하고, 데이터에 접근하여 값을 증가시킵니다. 마지막으로 메인 스레드에서 모든 스레드가 작업을 완료할 때까지 대기한 후, 최종 결과를 출력합니다.

원자적 참조 카운팅(Arc)

원자적 참조 카운팅(Arc, Atomic Reference Counting) 은 여러 스레드에서 안전하게 참조 카운트를 공유할 수 있도록 해주는 스마트 포인터(smart pointer)입니다. Arc<T>는 Rc<T>와 유사하지만, 스레드 안전성을 보장합니다.

use std::sync::Arc;
use std::thread;

fn main() {
    let data = Arc::new(vec![1, 2, 3, 4, 5]);

    let mut handles = vec![];

    for _ in 0..3 {
        let data = Arc::clone(&data);
        let handle = thread::spawn(move || {
            for val in data.iter() {
                println!("스레드 데이터: {}", val);
            }
        });
        handles.push(handle);
    }

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

위 예시에서 Arc::new(vec![1, 2, 3, 4, 5])를 통해 스레드 안전한 공유 데이터를 생성합니다. 각 스레드에서 Arc::clone(&data)를 사용하여 데이터의 참조를 복사하고, 해당 데이터를 반복하며 출력합니다.

 

Arc를 사용하면 여러 스레드에서 안전하게 데이터를 공유할 수 있습니다. 그러나 Arc는 불변 데이터에만 사용할 수 있으며, 가변 데이터의 경우 Mutex와 함께 사용해야 합니다.

동기적 병렬 실행(Synchronous Parallel Execution)

Rust에서는 std::thread::spawn 함수를 사용하여 비동기적으로 스레드를 생성합니다. 하지만 동기적인 병렬 실행이 필요한 경우에는 rayon 크레이트를 사용할 수 있습니다.

 

rayon 크레이트는 데이터 병렬화(data parallelism)를 위한 간편한 인터페이스를 제공합니다. 이터레이터 및 병렬 반복자(parallel iterators)를 활용하여 작업을 효율적으로 분산시킬 수 있습니다.

use rayon::prelude::*;

fn main() {
    let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    let result: Vec<_> = data.par_iter()
        .map(|x| x * x)
        .collect();

    println!("결과: {:?}", result);
}

위 예시에서 par_iter()를 사용하여 벡터의 요소를 병렬로 반복합니다. map 함수를 사용하여 각 요소를 제곱한 결과를 새로운 벡터에 수집합니다.

 

rayon 크레이트는 작업 분배를 자동으로 최적화하여 최대한의 성능을 발휘할 수 있도록 합니다. 또한, 스레드 풀(thread pool)을 사용하여 스레드 생성 및 관리의 오버헤드를 줄입니다.

비동기 프로그래밍(Asynchronous Programming)

Rust에서 비동기 프로그래밍을 위해서는 async/await  futures 크레이트를 사용합니다. 이를 통해 블록킹(blocking) 작업을 효율적으로 처리할 수 있습니다.

use std::time::Duration;
use futures::future::join_all;

#[tokio::main]
async fn main() {
    let task1 = async {
        println!("작업 1 시작");
        tokio::time::sleep(Duration::from_secs(2)).await;
        println!("작업 1 완료");
    };

    let task2 = async {
        println!("작업 2 시작");
        tokio::time::sleep(Duration::from_secs(1)).await;
        println!("작업 2 완료");
    };

    join_all(vec![task1, task2]).await;
    println!("모든 작업이 완료되었습니다.");
}

위 예시에서는 tokio 런타임을 사용하여 비동기 작업을 실행합니다. async 키워드를 사용하여 비동기 함수를 정의하고, await 키워드를 사용하여 비동기 작업의 완료를 기다립니다.

 

task1과 task2는 각각 2초와 1초 동안 대기한 후 완료됩니다. join_all 함수는 두 작업이 모두 완료될 때까지 기다린 후, 메인 함수를 계속 실행합니다.

 

비동기 프로그래밍은 블록킹 작업을 효율적으로 처리할 수 있도록 해줍니다. 특히 네트워크 통신, 파일 I/O, 데이터베이스 액세스 등의 작업에서 유용합니다. 그러나 비동기 코드는 작성하기 복잡할 수 있으므로, 적절한 경우에 사용하는 것이 중요합니다.

참고 자료

[1] The Rust Programming Language Book: https://doc.rust-lang.org/book/ch16-01-threads.html [2] Rust by Example: https://doc.rust-lang.org/rust-by-example/std_misc/threads.html [3] "Programming Rust" by Jim Blandy and Jason Orendorff (O'Reilly Media, 2021) [4] Rayon 공식 문서: https://crates.io/crates/rayon [5] Tokio 공식 문서: https://tokio.rs/

 

 

한 고대 문서 이야기

여기 한 고대 문서가 있습니다. 이 문서는 B.C. 1,500년 부터 A.D 100년까지 약 1,600 여 년 동안 기록되었습니다. 이 문서의 저자는 약 40 명입니다. 이 문서의 고대 사본은 25,000 개가 넘으나, 사본간 오

gospel79.tistory.com

 

유튜브 프리미엄 월 1만원 할인받고 월 4000원에 이용하는 방법

올해 5월부터 월 8000원 정도이던 유튜브 프리미엄 요금이 15000원 정도로 인상됩니다. 각종 OTT 서비스, ChatGPT 같은 서비스들이 늘어나다보니 이런 거 몇 개만 이용하더라도 월 이용요금이 5만원을

stock79.tistory.com

 

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."

728x90
반응형

댓글