동시성은 프로그램이 동일한 중앙 처리 장치(CPU) 코어를 사용하여 여러 작업을 동시에 수행할 수 있는 기능을 말합니다. 이러한 작업은 여러 프로세서가 있는 하드웨어를 사용하여 서로 다른 작업 또는 동일한 작업의 하위 작업을 동시에 실행하는 병렬 처리와는 달리 중첩된 방식으로 동시에 실행되고 종료됩니다.

Rust는 높은 수준의 안전성과 효율성을 유지하면서 뛰어난 성능과 동시 프로그래밍에 대한 강력한 지원으로 차별화됩니다. “두려움 없는 동시성”으로 알려진 Rust의 동시성 모델의 철학은 개발자가 엄격한 안전 지침을 준수하는 동시 코드를 작성할 수 있는 직관적인 수단을 제공하는 것입니다. 이는 컴파일 중에 엄격한 규칙을 적용하는 소유권 및 차용 시스템을 구현하여 데이터 추적과 관련된 잠재적인 문제를 방지하고 메모리 안정성을 보존함으로써 달성됩니다.

Rust의 동시성 이해

프로그래밍 언어 Rust는 스레드, 메시지 전달, 상호 제외 메커니즘, 원자 데이터 유형, `async/await`을 활용한 비동기 프로그래밍 등 동시 애플리케이션을 생성하기 위한 다양한 도구를 제공합니다.

Rust의 동시 구조에 대한 개요는 다음과 같습니다:

Rust 프로그래밍 언어는 여러 실행 스레드를 생성하고 관리하기 위해 `std::thread`라는 표준 라이브러리 구성 요소를 제공합니다. 스레드::스폰` 함수는 실행할 코드가 포함된 클로저를 전달하여 새 스레드를 스폰하는 데 사용할 수 있습니다. 또한 Rust는 스레드의 동시 실행을 지원하여 스레드의 상호 작용을 조절하는 동기화 프리미티브를 제공합니다. ‘차용 검사기’는 참조 사용이 의도하지 않은 동작으로 이어지는 것을 방지하는 역할을 합니다.

Rust에서 사용하는 동시 프로그래밍 패러다임에는 독립 스레드 간의 메시지 전달 활용이 통합되어 있습니다. 통신 프로세스는 채널을 구현하는 std::sync::mpsc 모듈을 통해 촉진됩니다. 각 채널은 발신자(송신자)와 수신자(수신자)로 구성되며, 스레드는 전자를 통해 메시지를 전달하고 후자를 통해 메시지를 수신할 수 있습니다. 이 메커니즘은 스레드 간 통신의 안전하고 조율된 방법을 보장합니다.

Rust는 여러 스레드 간에 상호 배타적인 데이터 공유 액세스를 보장하기 위해 std::sync::Mutex로 표시되는 상호 제외 메커니즘과 std::sync::atomic으로 표시되는 원자 유형을 비롯한 동기화 구성을 제공합니다.전자는 공유 데이터에 대한 액세스를 조정하여 데이터 경합 조건이 발생하는 것을 방지하는 반면, 후자는 명시적인 잠금 획득 없이 공유 데이터 구성 요소에서 원자 연산을 실행할 수 있게 해줍니다.

이 글도 확인해 보세요:  파이썬을 사용하여 FLAMES 게임 플레이하기

Rust에서 비동기/대기 및 퓨처를 활용하면 동시에 실행할 수 있는 비동기 프로그램을 개발할 수 있습니다. 이 기능을 사용하면 입출력 바인딩 작업을 효율적으로 처리할 수 있으므로 프로그램이 다른 입출력 작업의 결과를 기다리는 동안 여러 작업을 동시에 수행할 수 있습니다. Rust에서 사용하는 비동기 프로그래밍 모델은 퓨처에 의존하며, 이 패러다임을 구현하는 데 널리 사용되는 두 가지 런타임은 async-std 라이브러리와 tokio 라이브러리입니다.

Rust 스레드의 활용은 런타임 시 오버헤드가 최소화되어 성능 집약적인 애플리케이션에 적합한 선택이 될 수 있다는 특징이 있습니다. Rust의 동시성 프리미티브와 다양한 라이브러리 및 프레임워크의 통합을 통해 다양한 동시성 요구 사항을 다각도로 지원할 수 있습니다.

Rust에서 스레드 스폰을 사용하는 방법

std::thread 모듈의 활용은 주 스레드 또는 프로그램 내의 기존 스레드와 동시에 작동할 수 있는 추가 스레드를 생성할 수 있는 std::thread::spawn 함수를 통해 용이하게 이루어집니다.

`std::thread::spawn` 함수를 사용하면 `std::thread::id` 함수를 호출하여 스레드의 이름을 문자열 리터럴로 지정하는 인수를 전달하여 새 스레드를 생성할 수 있습니다. 이 함수는 새로 생성된 스레드의 고유 식별자를 반환하며, 이 식별자는 진행 상황을 추적하거나 스레드와 통신하는 데 사용될 수 있습니다.

 use std::thread;

fn main() {
    // Spawn a new thread
    let thread_handle = thread::spawn(|| {
        // Code executed in the new thread goes here
        println!("Hello from the new thread!");
    });

    // Wait for the spawned thread to finish
    thread_handle.join().unwrap();

    // Code executed in the main thread continues here
    println!("Hello from the main thread!");
}

주 함수의 주요 목적은 thread::spawn 함수를 사용하여 클로저를 제공함으로써 새로운 스레드를 시작하는 것입니다.

`thread_handle`에서 `join` 메서드를 사용하면 스폰된 스레드가 작업을 완료할 때까지 주 스레드가 완료를 보류할 수 있습니다. join`을 호출하면 스폰된 스레드가 완료될 때까지 주 스레드가 비활성 상태로 유지된 후 계속 진행됩니다.

다중 스레드 구현을 통해 병렬 처리를 활용하면 루프 또는 다른 Rust 제어 구조를 사용하여 나중에 별도의 스레드로 스폰되는 클로저 배열을 생성할 수 있습니다.

 use std::thread;

fn main() {
    let num_threads = 5;

    let mut thread_handles = vec![];

    for i in 0..num_threads {
        let thread_handle = thread::spawn(move || {
            println!("Hello from thread {}", i);
        });
        thread_handles.push(thread_handle);
    }

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

    println!("All threads finished!");
}

for 루프를 반복할 때마다 새 스레드가 생성되며, 각 스레드에는 고유 식별자 “i”와 루프 변수가 할당됩니다. 소유권 문제를 방지하기 위해 클로저는 “move” 키워드를 사용하여 “i”의 값을 캡처합니다.또한, “thread\_handles” 벡터는 “join” 루프에서 나중에 사용할 수 있도록 생성된 스레드를 저장하는 데 사용됩니다.

이 글도 확인해 보세요:  녹 매크로: 매크로를 사용하여 코드를 개선하는 방법

모든 스레드가 생성된 후 기본 함수는 스레드 핸들이 포함된 벡터를 탐색하고 각 요소에서 조인 메서드 호출을 실행한 후 모든 스레드가 실행을 완료할 때까지 대기 상태를 유지합니다.

채널을 통해 메시지 전달

스레드 안전 데이터 구조는 동시 액세스로 인해 데이터 경합이나 기타 동기화 문제가 발생하지 않도록 보장하기 위해 Rust에서 제공됩니다. 이러한 스레드 안전 데이터 구조에는 원자 변수, 뮤텍스 및 공유 가변 상태가 포함됩니다. 뮤텍스를 사용하면 다른 스레드가 동시에 실행되도록 허용하면서 코드나 데이터에 독점적으로 액세스할 수 있습니다. 공유 변경 가능 상태는 두 명의 작성자가 동시에 동일한 메모리 위치를 수정할 수 없도록 보장하므로, 읽는 사람은 많지만 쓰는 사람은 한 명뿐인 경우에 유용합니다. 또한 Rust의 소유권 모델을 사용하면 참조의 수명을 쉽게 추론하고 참조가 해제된 후 데이터에 액세스하는 것을 방지할 수 있습니다.

프로그램에서 메시지 전달을 위한 채널을 활용하여 스레드 간 통신을 수행할 수 있습니다.

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

fn main() {
    // Create a channel
    let (sender, receiver) = mpsc::channel();

    // Spawn a thread
    thread::spawn(move || {
        // Send a message through the channel
        sender.send("Hello from the thread!").unwrap();
    });

    // Receive the message in the main thread
    let received_message = receiver.recv().unwrap();
    println!("Received message: {}", received_message);
}

주어진 코드의 주요 목적은 파이썬의 “동시성” 모듈의 `mpsc::channel()` 함수를 활용하여 한 개체는 발신자 역할을 하고 다른 개체는 수신자 역할을 하는 두 개체 간의 통신 메커니즘을 설정하는 것입니다. 이 함수는 발신자와 수신자 간에 데이터를 전송할 수 있는 채널을 생성합니다. 그 후 프로그램은 여러 개의 스레드를 생성하고 클로저를 통해 발신자 객체의 소유권을 각 스레드의 로컬 범위로 이전합니다. 이러한 스레드 내에서 발신자는 설정된 채널을 통해 `sender.send()` 메서드를 사용하여 수신자에게 메시지를 보냅니다.

`receiver.recv()` 함수는 메시지가 수신될 때까지 기다렸다가 프로그램 실행을 중단하도록 설계되었습니다. 메시지가 성공적으로 수신되면 메인 함수는 메시지를 콘솔에 출력합니다.

이 글도 확인해 보세요:  HTTP와 HTTPS: 차이점은 무엇인가요?

매체를 통해 통신을 전송할 때 발신자가 소모됩니다. 메시지를 전송하는 데 여러 스레드가 필요한 경우 sender.clone() 메서드를 사용하여 발신자의 복제본을 만들 수 있습니다.

MPSC 모듈은 비차단 성격의 Try\_Recv() 메서드와 이전에 획득한 메시지에 액세스하기 위한 이터레이터를 생성하는 Iter() 함수를 포함하여 메시지 수신에 대한 몇 가지 대안을 제시합니다.

채널을 통한 메시지 전달을 활용하면 스레드 간 통신을 위한 안전하고 실용적인 방법을 제공하여 데이터 경합 발생을 방지하고 상호 조정을 보장할 수 있습니다.

메모리 안전성을 보장하는 Rust의 소유권 및 차용 모델

Rust는 소유권, 차용 및 차용 검사기를 조합하여 탄력성이 향상된 안전한 동시 프로그래밍 플랫폼을 제공합니다.

차용 검사기의 기능은 안정성을 위해 런타임 검사나 가비지 컬렉션에 의존하는 대신 컴파일 타임에 발생할 수 있는 문제를 식별하여 안전장치 역할을 하는 것입니다.

By 김민수

안드로이드, 서버 개발을 시작으로 여러 분야를 넘나들고 있는 풀스택(Full-stack) 개발자입니다. 오픈소스 기술과 혁신에 큰 관심을 가지고 있고, 보다 많은 사람이 기술을 통해 꿈꾸던 일을 실현하도록 돕기를 희망하고 있습니다.