Rust 를 이용한 병렬 데이터 처리

Beginner

This tutorial is from open-source community. Access the source code

소개

이 실습에서는 Rust 프로그래밍 언어를 사용하여 맵 - 리듀스 알고리즘으로 데이터 처리를 병렬화하는 방법을 살펴봅니다. 예제 코드는 데이터를 세그먼트로 나누고 각 세그먼트를 별도의 스레드에서 처리하여 숫자 블록의 모든 자릿수 합계를 계산합니다. Rust 표준 라이브러리는 데이터 경합을 방지하고 스레드 안전성을 보장하는 스레드 원시 요소를 제공합니다. 이 프로그램은 또한 Rust 가 스레드 경계를 가로질러 읽기 전용 참조를 전달하는 방식을 보여줍니다. 또한, 코드는 클로저, 반복자 및 join() 메서드를 사용하여 각 스레드의 중간 결과를 최종 합계로 결합하는 방법을 보여줍니다. 효율성을 위해 프로그램은 사용자 입력 데이터에 의존하는 대신 데이터를 제한된 수의 세그먼트로 분할하여 과도한 수의 스레드를 생성하는 것을 방지할 수 있습니다.

참고: 실습에서 파일 이름을 지정하지 않으면 원하는 파일 이름을 사용할 수 있습니다. 예를 들어 main.rs를 사용하고 rustc main.rs && ./main으로 컴파일 및 실행할 수 있습니다.

테스트 케이스: 맵 - 리듀스

Rust 는 전통적으로 이러한 시도와 관련된 많은 어려움 없이 데이터 처리를 쉽게 병렬화할 수 있도록 합니다.

표준 라이브러리는 훌륭한 스레드 원시 요소를 기본적으로 제공합니다. 이러한 요소는 Rust 의 소유권 및 참조 규칙 개념과 결합하여 데이터 경합을 자동으로 방지합니다.

참조 규칙 (하나의 쓰기 참조 XOR 여러 읽기 참조) 은 다른 스레드에서 볼 수 있는 상태를 조작하는 것을 자동으로 방지합니다. (동기화가 필요한 경우 Mutex 또는 Channel과 같은 동기화 원시 요소가 있습니다.)

이 예제에서는 숫자 블록의 모든 자릿수 합계를 계산합니다. 이 작업은 블록의 조각을 다른 스레드로 나누어 수행합니다. 각 스레드는 자체 작은 블록의 자릿수를 합산하고, 이후 각 스레드에서 생성된 중간 합계를 합산합니다.

스레드 경계를 가로질러 참조를 전달하더라도 Rust 는 읽기 전용 참조만 전달하고 있음을 이해하며, 따라서 안전성 위반이나 데이터 경합이 발생하지 않습니다. 또한 전달하는 참조의 수명이 'static이므로, 이러한 스레드가 여전히 실행 중일 때 데이터가 파괴되지 않음을 Rust 가 이해합니다. (스레드 간에 static이 아닌 데이터를 공유해야 하는 경우 Arc와 같은 스마트 포인터를 사용하여 데이터를 유지하고 static이 아닌 수명을 방지할 수 있습니다.)

use std::thread;

// 이것은 `main` 스레드입니다
fn main() {

    // 처리할 데이터입니다.
    // 스레드 기반 맵 - 리듀스 알고리즘을 통해 모든 자릿수의 합계를 계산합니다.
    // 각 공백으로 구분된 조각은 다른 스레드에서 처리됩니다.
    //
    // TODO: 공백을 삽입하면 출력이 어떻게 되는지 확인하십시오!
    let data = "86967897737416471853297327050364959
11861322575564723963297542624962850
70856234701860851907960690014725639
38397966707106094172783238747669219
52380795257888236525459303330302837
58495327135744041048897885734297812
69920216438980873548808413720956532
16278424637452589860345374828574668";

    // 자식 스레드를 보관할 벡터를 만듭니다.
    let mut children = vec![];

    /*************************************************************************
     * "맵" 단계
     *
     * 데이터를 세그먼트로 나누고 초기 처리를 적용합니다.
     ************************************************************************/

    // 개별 계산을 위한 데이터를 세그먼트로 분할합니다.
    // 각 조각은 실제 데이터에 대한 참조 (&str) 가 됩니다.
    let chunked_data = data.split_whitespace();

    // 데이터 세그먼트를 반복합니다.
    // .enumerate() 는 반복되는 항목에 현재 루프 인덱스를 추가합니다.
    // 결과 튜플 "(인덱스, 요소)"는 "구조 분해 할당"을 사용하여 즉시 "i"와 "data_segment"라는 두 변수로 분해됩니다.
    for (i, data_segment) in chunked_data.enumerate() {
        println!("data segment {} is \"{}\"", i, data_segment);

        // 각 데이터 세그먼트를 별도의 스레드에서 처리합니다.
        //
        // spawn() 는 새 스레드에 대한 핸들을 반환하며, 반환된 값에 액세스하려면 반드시 유지해야 합니다.
        //
        // 'move || -> u32'는 다음과 같은 클로저의 구문입니다.
        // * 인수를 받지 않습니다 ('||')
        // * 캡처된 변수의 소유권을 가져옵니다 ('move')
        // * 부호 없는 32 비트 정수를 반환합니다 ('-> u32')
        //
        // Rust 는 클로저 자체에서 '-> u32'를 충분히 추론하므로 생략할 수 있습니다.
        //
        // TODO: 'move'를 제거해보고 무슨 일이 발생하는지 확인하십시오.
        children.push(thread::spawn(move || -> u32 {
            // 이 세그먼트의 중간 합계를 계산합니다.
            let result = data_segment
                        // 세그먼트의 문자를 반복합니다.
                        .chars()
                        // 텍스트 문자를 숫자 값으로 변환합니다.
                        .map(|c| c.to_digit(10).expect("should be a digit"))
                        // 결과 숫자 반복자를 합산합니다.
                        .sum();

            // println! 는 stdout 을 잠그므로 텍스트가 겹치지 않습니다.
            println!("processed segment {}, result={}", i, result);

            // "return"은 필요하지 않습니다. Rust 는 "표현식 언어"이기 때문입니다. 각 블록에서 마지막으로 평가된 표현식이 자동으로 값이 됩니다.
            result

        }));
    }


    /*************************************************************************
     * "리듀스" 단계
     *
     * 중간 결과를 수집하고 최종 결과로 결합합니다.
     ************************************************************************/

    // 각 스레드의 중간 결과를 단일 최종 합계로 결합합니다.
    //
    // sum() 에 형식 힌트를 제공하기 위해 "터보피쉬" ::<>를 사용합니다.
    //
    // TODO: 대신 명시적으로 final_result 의 형식을 지정하여 터보피쉬 없이 시도하십시오.
    let final_result = children.into_iter().map(|c| c.join().unwrap()).sum::<u32>();

    println!("최종 합계 결과: {}", final_result);
}

과제

스레드 수가 사용자 입력 데이터에 따라 달라지도록 하는 것은 좋지 않습니다. 사용자가 많은 공백을 삽입하면 어떻게 될까요? 실제로 2,000 개의 스레드를 생성하고 싶습니까? 프로그램을 수정하여 데이터가 항상 프로그램 시작 부분의 정적 상수로 정의된 제한된 수의 조각으로 분할되도록 합니다.

요약

축하합니다! 맵 - 리듀스 (Map-Reduce) 실습을 완료했습니다. LabEx 에서 더 많은 실습을 통해 기술을 향상시킬 수 있습니다.