Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

메시지 전달로 스레드 사이에 데이터 보내기

안전한 동시성을 보장하기 위한 점점 더 인기 있는 접근법 중 하나는 메시지 전달(message passing) 입니다. 이 방식에서는 스레드나 액터가 데이터를 담은 메시지를 서로 주고받으며 통신합니다. Go 언어 문서의 유명한 구호를 떠올려 보세요. “공유 메모리로 통신하지 말고, 통신함으로써 메모리를 공유하라.”

그렇다면 “메모리를 공유하면서 통신한다”는 것은 어떤 모습일까요? 또 왜 메시지 전달을 선호하는 사람들은 메모리 공유를 피하라고 이야기할까요?

어떤 의미에서 모든 언어의 채널은 단일 소유권과 비슷합니다. 값을 채널을 통해 보내면 그 값을 더 이상 쓰지 않아야 하기 때문입니다. 반면 공유 메모리 기반 동시성은 다중 소유권과 비슷합니다. 여러 스레드가 동시에 같은 메모리 위치에 접근할 수 있기 때문입니다. 15장에서 여러 소유권을 가능하게 해 주는 스마트 포인터가 얼마나 복잡성을 더하는지 보았습니다. 서로 다른 소유자들을 관리해야 했기 때문입니다. 러스트의 타입 시스템과 소유권 규칙은 이 관리를 올바르게 하는 데 큰 도움을 줍니다. 이제 공유 메모리의 흔한 동시성 원시 도구 중 하나인 mutex를 예로 들어 이를 살펴보겠습니다.

뮤텍스로 접근 제어하기

Mutexmutual exclusion 의 줄임말로, 어떤 순간에도 오직 하나의 스레드만 특정 데이터에 접근할 수 있게 해 줍니다. 뮤텍스 안의 데이터에 접근하려면, 먼저 스레드가 접근 권한을 원한다고 알려 락(lock)을 획득해야 합니다. 은 현재 누가 그 데이터에 대한 배타적 접근권을 가지고 있는지를 추적하는, 뮤텍스의 일부인 데이터 구조입니다. 그래서 흔히 뮤텍스가 자신이 담고 있는 데이터를 “락으로 지킨다”고 표현합니다.

뮤텍스는 두 가지 규칙을 기억해야 해서 사용하기 어렵다는 평판이 있습니다.

  1. 데이터를 사용하기 전에 반드시 락을 획득하려고 시도해야 한다.
  2. 뮤텍스가 지키는 데이터를 다 쓴 뒤에는, 다른 스레드가 락을 잡을 수 있도록 반드시 락을 해제해야 한다.

실생활 비유로는, 하나의 마이크만 있는 컨퍼런스 패널 토론을 떠올려 보면 됩니다. 패널 한 명이 말하고 싶으면 먼저 마이크를 쓰고 싶다고 요청해야 하고, 마이크를 받으면 원하는 만큼 말할 수 있습니다. 그리고 나면 마이크를 다음으로 말하고 싶은 사람에게 넘겨줘야 합니다. 만약 누군가가 다 말한 뒤에도 마이크를 넘기지 않으면, 아무도 더 이상 말을 할 수 없게 됩니다. 공유 마이크 관리가 꼬이면 패널 전체가 엉망이 되는 것이지요.

뮤텍스 관리는 올바르게 구현하기가 정말 까다로울 수 있기 때문에, 많은 사람들이 채널에 열광합니다. 하지만 러스트에서는 타입 시스템과 소유권 규칙 덕분에 락 획득과 해제를 잘못하는 일이 훨씬 어렵습니다.

Mutex<T> API

뮤텍스를 어떻게 쓰는지 보기 위해, 먼저 단일 스레드 환경에서 단순하게 사용해 보겠습니다. 목록 16-12를 보세요.

Filename: src/main.rs
use std::sync::Mutex;

fn main() {
    let m = Mutex::new(5);

    {
        let mut num = m.lock().unwrap();
        *num = 6;
    }

    println!("m = {m:?}");
}
Listing 16-12: 단순화를 위해 단일 스레드 문맥에서 Mutex<T> API 살펴보기

많은 타입과 마찬가지로, Mutex<T> 도 연관 함수 new 로 만듭니다. 뮤텍스 안의 데이터에 접근하려면 lock 메서드를 사용해 락을 획득합니다. 이 호출은 현재 스레드를 블록시켜, 락을 얻을 차례가 올 때까지 다른 일을 하지 못하게 합니다.

락을 쥐고 있던 다른 스레드가 패닉하면 lock 호출은 실패할 수 있습니다. 그런 상황에서는 아무도 그 락을 더 이상 얻지 못할 수 있으므로, 여기서는 unwrap 을 사용해 그런 경우 현재 스레드도 패닉하게 하기로 했습니다.

락을 획득한 뒤에는, 여기서 num 이라는 이름의 반환값을 뮤텍스 안 데이터에 대한 가변 참조처럼 다룰 수 있습니다. 타입 시스템 덕분에, m 안 값을 사용하기 전에 반드시 락을 획득하게 됩니다. m 의 타입은 Mutex<i32> 이지 i32 가 아니기 때문에, 우리는 반드시 lock 을 호출해야만 안의 i32 값에 접근할 수 있습니다. 이 과정을 잊어버릴 수 없습니다. 타입 시스템이 그걸 허용하지 않기 때문입니다.

lock 호출은 LockResult 로 감싼 MutexGuard 타입을 반환하며, 우리는 unwrap 으로 이를 처리했습니다. MutexGuard 타입은 안쪽 데이터를 가리키도록 Deref 를 구현하고 있고, 또한 Drop 구현을 가지고 있어 MutexGuard 가 스코프를 벗어나는 순간 락을 자동으로 해제합니다. 이 예제에서는 내부 스코프가 끝날 때 그렇게 됩니다. 덕분에 락 해제를 잊어 다른 스레드가 뮤텍스를 사용하지 못하게 막아 버릴 위험이 줄어듭니다.

락이 해제된 뒤에는 뮤텍스 값을 출력하고, 안의 i32 값을 6 으로 바꿀 수 있었다는 사실을 확인할 수 있습니다.