[Rust] Arc, Mutex (스레드의 데이터 공유)
5. Arc, Mutex. (스레드의 데이터 공유)
스레드 사이에 데이터를 교환하는 방법은 크게 두 가지가 있습니다.
앞 장에서 본 Message passing과 이번 장에서 볼 데이터 공유입니다.
Message passing은 채널을 통해서 데이터를 진짜 주고받는 방식입니다.
반면에 데이터 공유 방식은 데이터를 주고 받는 대신 메모리의 데이터를 공유해서 스레드 사이의 통신을 달성하는 방법입니다.
Rust는 스레드 사이의 데이터 공유를 달성하기 위해 Arc와 Mutex를 사용합니다.
Arc와 Mutex는 스마트 포인터입니다.
이미 공부했던 스마트 포인터, Rc, Cell, RefCell 등은 하나의 스레드에 자리 잡고 있습니다.
다른 스레드의 스마트 포이터와 그 데이터를 사용할 수는 없습니다.
Arc는 공유를 통해 이런 일을 가능하게 합니다.
Arc는 Atomic Reference counting의 약자입니다. 즉 atomic Rc입니다.
atomic type 개념은 C에도 있고 Java에도 있습니다.
어떤 변수에 접근하려는 것을 방해해서 중단시키는 일을 방지하기 위한 타입입니다.
쉽게 말하자면 한 스레드가 어떤 변수를 사용하려고 접근을 시작화면 사용을 마찰 때까지 다른 스레드가 방해할 수 없음을 보장하는 타입입니다.
비유하자면 우리가 도서관에서 책을 빌려오면 우리가 다시 반납할 때까지 다른 이용자들의 방해를 받지 않고 마치 우리 소유인 것처럼 책을 사용할 수 있는 것과 같습니다.
이론은 그렇지만 스레드를 사용하는 우리 입장에서 중요한 점은 atomic은 여러 스레드가 한꺼번에 데이터를 사용하려고 해도 별 문제가 발생하지 않는 타입이라는 것입니다.
단일 스레드에서 하나의 데이터에 복수의 소유권을 허용하는 타입이 Rc 였습니다.
Arc는 기본적으로 Rc와 같지만 단일 스레드가 아닌 곳에 사용 됩니다.
Mutext는 Mutual exclusion의 약자입니다.
한 번에 하나의 스레드, 하나의 변수만 데이터를 사용할 수 있게 합니다.
이름에서도 알 수 있듯이 “상호 간에 배타적”입니다.
즉 하나가 값을 사용하고 있으면 다른 스레드의 변수는 값을 사용하지 못합니다.
Mutext는 데이터에 대한 독점권을 보장합니다.
Mutext를 사용할 때는 lock 메서드를 사용해서 값을 사용할 권리를 얻어야 합니다. lock 메서드는 “이제 내가 이 데이터 찜. 다른 애들은 접근하지 마!”하는 것과 비슷합니다.
lock 메서드의 반환 값 타입은 MutexGuard입니다.
MutexGuard 타입은 다른 스레드가 데이터에 접근하는 것에서 데이터를 보호합니다.
그래서 이름에도 Guard가 들어갑니다.
이렇게 보호받는 값을 사용할 때는 Result 타입처럼 unwrap 등의 메서드를 사용합니다.
예제를 통해 Mutex를 살펴보겠습니다.
use std::sync::{Arc, Mutex};
fn main() {
let mt = Mutex::new(0); // 1
let mut num = mt.lock().unwrap(); // 2
*num = 100; // 3
println!("{:#?}", mt); // Mutex 자체를 출력
println!("{}", num); // Mutex에 들어 있던 값을 출력
}
// 출력
Mutex {
data: <locked>,
poisoned: false,
..
}
100
// 1: mt의 타입은 Mutex<i32>입니다,
// 2: num의 타입은 MurtexGuard<i32>입니다. lock 메서드는 Mutex 타입의 값을 사용할 수 있게 합니다.
// 3: 역침조를 통해 num : MurtexGuard<i32>에 들어 있는 데이터에 접근하고 변경합니다.
// 출력: Mutex<i32> 타입인 변수 mt 안의 data값이 <locked>입니다.
즉 잠겨서 접근할 수 없습니다. lock 메서드는 Mutex에 감춰진 값을 사용할 수 있게 합니다.
하지만 Mutex 특성상 한 번에 하나의 스레드에만 하나의 소유권을 허용합니다.
이 경우에는 변수 num에게 Mutex가 가지고 있던 값의 사용이 허용되었고 mt에게는 잠겨 있습니다.
다음과 같이 코드를 바꿔 보겠습니다.
use std::sync::{Arc, Mutex};
fn main() {
let mt = Mutex::new(0);
{
let mut num = mt.lock().unwrap();
*num = 100;
} // 1
println!("{:#?}", mt); // 2
println!("{}", mt.lock().unwrap()); // 3
}
// 출력
Mutex {
data: 100,
poisoned: false,
..
}
100
// 1: 스코프가 설정되어 있는 것을 제외하고는 앞의 코드와 같습니다.
스코프를 벗어나면 num의 수명이 다 하고, lock은 해제됩니다.
lock이 해제되었다는 것은 데이터를 사용할 수 있는 독점 권한이 사라지는 것입니다.
그리고 다시 mt가 데이터를 사용할 수 있게 됩니다.
그래서 // 2의 출력 부분을 보면 data에 100이 들어 있습니다.
그리고 mt의 값만 추출해 보기 위해 // 3으로 출력해 보면 100이 나옵니다.
사실 데이터는 Mutex인 mt를 떠난 적이 없습니다.
독점 사용 권한만 잠시 num에게 주어졌다가 회수된 것뿐입니다.
Mutex는 스마트 포인터로서, 다른 스마트 포인터처럼 저장한 데이터를 가지고 있습니다.
다른 스마트 포인터에 비해 특이한 것은 값을 사용하기 위해 lock 메서드를 사용해야 한다는 것입니다.
Mutex는 한 번에 하나의 접근만 허용하는 배타적 성격을 보장합니다. 즉 독점권을 보장합니다.
이제 복수의 소유권을 허락할 뿐만 아니라 복수의 스레드에서도 사용할 수 있는 Arc 타입을 살펴보겠습니다.
한 스레드 안에서 일어난 데이터의 변화가 다른 스레드에도 영향을 미치는 경우를 보겠습니다.
즉 데이터의 공유를 확인해 보겠습니다.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let ar = Arc::new(Mutex::new(0)); // 1
let ar1 = ar.clone(); // 2
thread::spawn(move || {
*ar1.lock().unwrap() = 100; // 3
println!("ar1 in thread1: {:#?}", ar1); // 4
ar1
}).join().expect("fail!");
println!("ar in main: {:#?}", ar); // 5
}
// 출력
ar1 in thread1: Mutex {
data: 100,
poisoned: false,
..
}
ar in main: Mutex {
data: 100,
poisoned: false,
..
}
// 1: Arc를 생성했습니다. Mutex의 데이터는 0입니다.
// 2: ar을 클론 해서 ar1을 만들었습니다.
// 3: 새로 만든 스레드에 ar1을 전달하고 데이터를 변경해 100으로 만들었습니다.
// 4와 //5: 다른 스레드에 있는 ar1과 ar을 각각 출력해 보면, 데이터의 공유가 일어나 두 경우 모두 Mutex의 데이터 값이 100으로 출력됩니다.
이번에는 공유하는 스레드를 하나 늘려서 테스트해 보겠습니다.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let ar = Arc::new(Mutex::new(0)); // 1
println!("first, ar in main: {:?}", ar); // 2
let ar1 = ar.clone(); // 3
thread::spawn(move || { // 스레드 1
*ar1.lock().unwrap() = 150; // 4
println!("ar1 in thread1: {:?}", ar1);
}).join().expect("fail!");
println!("second, ar in main: {:?}", ar);
let ar2 = ar.clone(); // 5
thread::spawn(move || { // 스레드 2
*ar2.lock().unwrap() += 300; // 6
println!("ar2 in thread2: {:?}", ar2);
}).join().expect("fail!");
println!("third, ar in main: {:?}", ar);
}
// 출력
first, ar in main: Mutex { data: 0, poisoned: false, .. }
ar1 in thread1: Mutex { data: 150, poisoned: false, .. }
second, ar in main: Mutex { data: 150, poisoned: false, .. }
ar2 in thread2: Mutex { data: 450, poisoned: false, .. }
third, ar in main: Mutex { data: 450, poisoned: false, .. }
// 출력의 data를 추적해 보면 데이터의 공유를 확인할 수 있습니다.
// 1: Arc 타입 변수 ar를 생성했습니다. 초기 데이터는 Mutex::new(0) 입니다.
// 2: 확인을 위해 ar를 출력했습니다.
// 3: ar의 클론 ar1을 만들었습니다.
// 4: 새로운 스레드 1 안에서 ar1의 데이터에 변화를 주었습니다.
// 5: ar의 새로운 클론 ar2를 만들었습니다.
// 6: 새로운 스레드 2 안에서 “+=“ 연산으로 기존 데이터에 300을 더했습니다. 이 연산이 일어나기 전에 ar2의 Mutex는 150을 가지고 있었고 연산 결과 450이 되었습니다.
가장 마지막으로 출력한 main 스레드의 ar에도 이런 변화가 반영되어 있습니다.
이상을 통해서 Arc를 통한 데이터의 공유를 확인할 수 있습니다.