English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Rust 병행 프로그래밍

병행을 안전하고 효율적으로 처리하는 것은 Rust가 탄생한 목적 중 하나로, 주로 서버 고 부하耐受성을 해결합니다.

병행(concurrent)의 개념은 프로그램의 다른 부분이 독립적으로 실행되며, 이는 병행(parallel)의 개념과 쉽게 혼동할 수 있습니다. 병행은 "동시에 실행"을 강조합니다.

병행은 일반적으로 병행으로 이어집니다.

이 장은 병행과 관련된 프로그래밍 개념과 세부 사항에 대해 설명합니다.

스레드

스레드(thread)는 프로그램에서 독립적으로 실행되는 하나의 부분입니다.

스레드는 프로세스(process)와 다른 점은 스레드는 프로그램 내부의 개념이며, 프로그램은 일반적으로 하나의 프로세스에서 실행됩니다.

운영체제 환경에서는 일반적으로 프로세스가 교차하여 스케줄링되어 실행되며, 스레드는 프로세스 내에서 프로그램에 의해 스케줄링됩니다.

스레드 병행이 병행 상황이 발생할 가능성이 있기 때문에, 병행에서 겪을 수 있는 락(Deadlock), 지연 오류가 자주 발생하는 프로그램에서는 병행 메커니즘이 포함되어 있습니다.

이러한 문제를 해결하기 위해 많은 다른 언어(예: Java, C#)는 특별한 런타임@RunWith 소프트웨어를 사용하여 자원을 조정하지만, 이는 프로그램의 실행 효율성을 크게 저하시킵니다.

C/C++ 언어는 운영체제의 가장 하위 층에서도 멀티 스레드를 지원하며, 언어 자체와 그 컴파일러는 병행 오류를 탐지하고 방지할 수 있는 능력이 없어 개발자에게 큰 부담이 됩니다. 개발자는 오류가 발생하지 않도록 많은 노력을 기울여야 합니다.

Rust는 실행 시간 환경에 의존하지 않으며, 이는 C와 같습니다./C++ 같습니다.

하지만 Rust는 언어 자체에서 소유권 기계를 포함한 수단을 설계하여 가장 일반적인 오류를 컴파일 단계에서 제거하려고 합니다. 이는 다른 언어에서는 없는 점입니다.

하지만 이는 우리가 프로그래밍할 때 무심코 할 수 있는 것이 아니며, 현재 동기화로 인해 발생하는 문제는 공개된 범위에서 완전히 해결되지 않았으며, 오류가 발생할 가능성이 있습니다. 동기 프로그래밍을 할 때는 주의해야 합니다!

Rust에서는 std::thread::spawn 함수를 통해 새 프로세스를 생성합니다:

use std::thread;
use std::time::Duration;
fn spawn_function() {
    for i in 0..5 {
        println!("spawned thread print {}", i);
        thread::sleep(Duration::from_millis(1));
    }
}
fn main() {
    thread::spawn(spawn_function);
    for i in 0..3 {
        println!("main thread print {}", i);
        thread::sleep(Duration::from_millis(1));
    }
}

실행 결과:

메인 스레드 출력 0
스폰된 스레드 출력 0
메인 스레드 출력 1
스폰된 스레드 출력 1
메인 스레드 출력 2
스폰된 스레드 출력 2

이 결과는 일부 경우에 순서가 변경될 수 있지만, 일반적으로 이렇게 출력됩니다.

이 프로그램에는 출력을 위해 있는 서브 스레드가 있습니다. 5 행문, 메인 스레드는 세 줄의 텍스트를 출력하지만, 명백하게도 메인 스레드가 종료되면 스폰된 스레드도 종료되어 모든 출력이 완료되지 않았습니다.

std::thread::spawn 함수의 매개변수는 매개변수가 없는 함수이지만, 위의 작성 방법은 권장되지 않습니다. 클로저(closures)를 사용하여 함수를 매개변수로 전달할 수 있습니다:

use std::thread;
use std::time::Duration;
fn main() {
    thread::spawn(|| {
        for i in 0..5 {
            println!("spawned thread print {}", i);
            thread::sleep(Duration::from_millis(1));
        }
    });
    for i in 0..3 {
        println!("main thread print {}", i);
        thread::sleep(Duration::from_millis(1));
    }
}

클로저는 변수에 저장되거나 다른 함수에 매개변수로 전달할 수 있는 익명 함수입니다. 클로저는 Rust의 Lambda 표현식과 동일하며 다음과 같은 형식으로 작성됩니다:

|매개변수1, 매개변수2, ...| -) > 반환 값 타입 {
    // 함수체
}

예를 들어:

fn main() {
    let inc = |num: i32| -) > i32 {
        num + 1
    });
    println!("inc(5) = {}, inc(5));
}

실행 결과:

inc(5) = 6

클로저는 타입 선언을 생략할 수 있으며 Rust의 자동 타입 추론 기계를 사용할 수 있습니다:

fn main() {
    let inc = |num| {
        num + 1
    });
    println!("inc(5) = {}, inc(5));
}

결과가 변하지 않았습니다.

join 메서드

use std::thread;
use std::time::Duration;
fn main() {
    let handle = thread::spawn(|| {
        for i in 0..5 {
            println!("spawned thread print {}", i);
            thread::sleep(Duration::from_millis(1));
        }
    });
    for i in 0..3 {
        println!("main thread print {}", i);
        thread::sleep(Duration::from_millis(1));
    }
    handle.join().unwrap();
}

실행 결과:

메인 스레드 출력 0 
스폰된 스레드 출력 0 
스폰된 스레드 출력 1 
메인 스레드 출력 1 
스폰된 스레드 출력 2 
메인 스레드 출력 2 
스폰된 스레드 출력 3 
스폰된 스레드 출력 4

join 메서드는 서브 스레드가 실행되고 끝날 때까지 프로그램을 멈추지 않도록 합니다.

move 강제 소유권 이전

이는 자주 발생하는 상황입니다:

use std::thread;
fn main() {
    let s = "hello";
    
    let handle = thread::spawn(|| {
        println!("{}", s);
    });
    handle.join().unwrap();
}

서브 스레드에서 현재 함수의 자원을 사용하려고 시도하는 것은 항상 잘못입니다! 소유권 메커니즘은 이러한 위험한 상황이 발생하지 않도록 막고 있으며, 이는 소유권 메커니즘의 자원 파괴 일관성을 깨뜨릴 것입니다. 우리는 클로저의 move 키워드를 사용하여 처리할 수 있습니다:

use std::thread;
fn main() {
    let s = "hello";
    
    let handle = thread::spawn(move || {
        println!("{}", s);
    });
    handle.join().unwrap();
}

메시지 전달

Rust에서 메시지 전달과 병행을 구현하는 주요 도구는 채널(channel)입니다. 채널은 보내기자(transmitter)와 수신자(receiver) 두 부분으로 구성됩니다.

std::sync::mpsc는 메시지 전달 메서드를 포함하고 있습니다:

use std::thread;
use std::sync::mpsc;
fn main() {
    let (tx, rx) = mpsc::channel();
    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
    });
    let received = rx.recv().unwrap();
    println!("받은: {}", received);
}

실행 결과:

받은: hi

서브 스레드가 메인 스레드의 보내기자 tx를 받아서 그의 send 메서드를 호출하여 문자열을 보냈고, 그런 다음 메인 스레드가 해당 수신자 rx를 통해 수신했습니다.