1
친구 여러분, 이번에는 Rust에서 동기화 기술과 채널을 활용하는 방법에 대해 자세히 알아보겠습니다. 동시성 프로그래밍에서 가장 중요한 것은 바로 데이터 경합과 데드락을 방지하는 것입니다. 이를 위해서는 적절한 동기화 기술과 채널을 사용해야 합니다. 이 부분을 잘 익히면 안전하고 효율적인 동시성 프로그램을 작성할 수 있습니다.
뮤텍스(Mutex)
뮤텍스(Mutex, Mutual Exclusion) 는 여러 스레드가 공유 데이터에 접근하는 것을 제어하여 데이터 경합을 방지하는 동기화 기술입니다. Rust에서는 std::sync 모듈의 Mutex 타입을 통해 뮤텍스를 사용할 수 있습니다.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut value = counter.lock().unwrap();
*value += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("최종 카운터 값: {}", *counter.lock().unwrap());
}
위 예제에서는 Mutex::new(0)를 사용하여 counter라는 공유 데이터를 정의했습니다. 각 스레드에서는 lock 메서드를 호출하여 뮤텍스를 획득한 후, 공유 데이터에 접근합니다. 이를 통해 한 번에 하나의 스레드만 counter에 접근할 수 있게 됩니다.
뮤텍스를 사용할 때는 다음과 같은 주의사항이 있습니다:
- 데드락 방지: 뮤텍스를 획득한 상태에서 다른 뮤텍스를 기다리면 데드락이 발생할 수 있습니다. 따라서 가능한 한 뮤텍스를 획득하는 시간을 최소화해야 합니다.
- 중첩 잠금 방지: 한 스레드에서 동일한 뮤텍스를 두 번 이상 잠그려고 시도하면 안 됩니다. 이는 데드락을 유발할 수 있습니다.
- 뮤텍스 해제 보장: 뮤텍스를 획득했다면 반드시 해제해야 합니다. 그렇지 않으면 다른 스레드가 영원히 기다리게 될 수 있습니다.
뮤텍스는 작은 크리티컬 섹션(critical section)에서 사용하는 것이 좋습니다. 크리티컬 섹션이 커지면 병목 현상이 발생할 수 있기 때문입니다. 또한 가능한 한 Arc와 같은 스레드 안전 타입을 사용하여 공유 데이터를 관리하는 것이 바람직합니다.
채널(Channel)
채널은 스레드 간 메시지 전달 방식으로, 데이터를 안전하게 전송할 수 있습니다. Rust에서는 std::sync::mpsc 모듈을 통해 채널을 사용할 수 있습니다.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
let handle = thread::spawn(move || {
tx.send(42).unwrap();
});
let received = rx.recv().unwrap();
println!("새로운 스레드로부터 받은 값: {}", received);
handle.join().unwrap();
}
위 예제에서 mpsc::channel()을 사용하여 채널을 생성했습니다. 이때 반환되는 (tx, rx) 튜플에서 tx는 송신 끝, rx는 수신 끝입니다. 새로운 스레드에서는 tx.send(42)로 42라는 값을 보내고, 메인 스레드에서는 rx.recv()로 해당 값을 받습니다.
채널을 사용할 때는 다음과 같은 점에 주의해야 합니다:
- 소유권 이전: 데이터를 전송할 때 소유권이 송신 끝에서 수신 끝으로 이전됩니다.
- 다중 생산자/소비자 채널: 단일 생산자-단일 소비자, 다중 생산자-단일 소비자, 그리고 다중 생산자-다중 소비자 채널이 있습니다.
- 버퍼링 지원: 채널은 내부적으로 버퍼링을 지원하므로, 송신 끝에서 보낸 데이터가 즉시 수신되지 않아도 안전합니다.
- 채널 종료: 모든 송신 끝이 닫히면 채널이 종료됩니다. 이후에는 더 이상 데이터를 전송할 수 없습니다.
채널은 스레드 간 통신을 안전하게 해주는 매우 유용한 도구입니다. 특히 생산자-소비자 문제를 해결할 때 유용하게 사용될 수 있습니다. 하지만 채널을 무작정 남용하면 오버헤드가 증가할 수 있으므로, 상황에 맞게 적절히 사용해야 합니다.
세마포어(Semaphore)
세마포어는 자원 제한을 구현하는 동기화 기술입니다. 특정 자원에 대한 접근 횟수를 제한할 수 있습니다. Rust에서는 std::sync 모듈의 Semaphore 타입을 통해 세마포어를 사용할 수 있습니다.
use std::sync::{Arc, Semaphore, SemaphorePermit};
use std::thread;
fn main() {
let semaphore = Arc::new(Semaphore::new(2));
let mut handles = vec![];
for _ in 0..5 {
let semaphore = Arc::clone(&semaphore);
let handle = thread::spawn(move || {
let _permit = semaphore.acquire().unwrap();
// 제한된 리소스에 대한 작업 수행
println!("스레드가 리소스에 접근했습니다.");
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
}
위 예제에서는 Semaphore::new(2)를 사용하여 최대 2개의 스레드만 동시에 리소스에 접근할 수 있도록 제한했습니다. 각 스레드에서는 acquire 메서드를 호출하여 세마포어 허가를 얻고, 작업을 수행한 후 세마포어 허가를 반환합니다.
세마포어를 사용할 때는 다음과 같은 점에 주의해야 합니다:
- 허가 획득 실패 처리: acquire 메서드가 실패할 경우 에러 처리를 해야 합니다.
- 허가 반환 보장: 획득한 세마포어 허가는 반드시 반환해야 합니다. 그렇지 않으면 다른 스레드가 영원히 기다리게 될 수 있습니다.
- 과도한 세마포어 생성 방지: 세마포어를 너무 많이 생성하면 오버헤드가 증가할 수 있습니다.
세마포어는 풀링 기법에서 자주 사용됩니다. 예를 들어, 데이터베이스 커넥션 풀이나 스레드 풀에서 세마포어를 사용하여 리소스 접근을 제한할 수 있습니다. 이를 통해 과도한 리소스 소모를 방지할 수 있습니다.
조건 변수(Condition Variable)
조건 변수는 스레드가 특정 조건이 만족될 때까지 기다리게 하는 동기화 기술입니다. Rust에서는 std::sync 모듈의 Condvar 타입을 통해 조건 변수를 사용할 수 있습니다.
use std::sync::{Arc, Condvar, Mutex};
use std::thread;
fn main() {
let pair = Arc::new((Mutex::new(false), Condvar::new()));
let pair_cloned = Arc::clone(&pair);
let handle = thread::spawn(move || {
let &(ref lock, ref cvar) = &*pair_cloned;
let mut started = lock.lock().unwrap();
while !*started {
started = cvar.wait(started).unwrap();
}
println!("스레드가 시작되었습니다!");
});
let &(ref lock, ref cvar) = &*pair;
let mut started = lock.lock().unwrap();
*started = true;
cvar.notify_one();
handle.join().unwrap();
}
위 예제에서는 (Mutex<bool>, Condvar) 튜플을 정의했습니다. 새로운 스레드에서는 while !*started 조건이 만족될 때까지 cvar.wait를 호출하여 기다립니다. 메인 스레드에서는 started를 true로 설정한 후 notify_one을 호출하여 대기 중인 스레드를 깨웁니다.
조건 변수를 사용할 때는 다음과 같은 점에 주의해야 합니다:
- 뮤텍스와 함께 사용: 조건 변수는 항상 뮤텍스와 함께 사용해야 합니다. 이는 데이터 경합을 방지하기 위함입니다.
- 스펄러스 웨이크업 방지: 스레드가 필요 이상으로 깨어나는 스펄러스 웨이크업 문제를 방지해야 합니다.
- 조건 변경 후 신호 전달: 조건을 변경한 후에는 반드시 신호를 전달해야 합니다. 그렇지 않으면 스레드가 영원히 기다리게 될 수 있습니다.
조건 변수는 주로 생산자-소비자 문제나 작업 큐 관리 등에 사용됩니다. 또한 여러 스레드가 특정 조건을 기다려야 할 때 유용하게 사용될 수 있습니다.
One-time 초기화와 배리어
One-time 초기화는 전역 변수나 정적 변수를 안전하게 초기화하는 방법입니다. Rust에서는 std::sync::Once 타입을 통해 one-time 초기화를 구현할 수 있습니다.
use std::sync::Once;
static mut GLOBAL: &str = "초기화되지 않음";
static INIT: Once = Once::new();
fn main() {
INIT.call_once(|| {
unsafe {
GLOBAL = "안전하게 초기화됨";
}
});
println!("{}", unsafe { GLOBAL });
}
위 예제에서는 GLOBAL이라는 정적 변수를 정의했습니다. INIT 객체의 call_once 메서드를 호출하면, 해당 클로저가 한 번만 실행되어 GLOBAL을 안전하게 초기화합니다.
배리어(Barrier) 는 여러 스레드가 특정 지점에서 동기화되도록 합니다. Rust에서는 std::sync 모듈의 Barrier 타입을 통해 배리어를 사용할 수 있습니다.
use std::sync::Arc;
use std::sync::Barrier;
use std::thread;
fn main() {
let mut handles = Vec::with_capacity(10);
let barrier = Arc::new(Barrier::new(10));
for _ in 0..10 {
let c = barrier.clone();
handles.push(thread::spawn(move || {
println!("스레드 준비됨!");
c.wait();
println!("스레드 계속 실행 중...");
}));
}
for handle in handles {
handle.join().unwrap();
}
}
위 예제에서는 Barrier::new(10)을 사용하여 10개의 스레드가 동기화되도록 합니다. 각 스레드는 barrier.wait()를 호출하여 다른 스레드가 도착할 때까지 기다린 후, 모든 스레드가 도착하면 계속 실행됩니다.
One-time 초기화와 배리어
One-time 초기화
One-time 초기화는 전역 변수나 정적 변수를 단 한 번만 초기화하는 것을 말합니다. 이는 여러 스레드에서 동시에 초기화를 시도할 때 발생할 수 있는 데이터 경합 문제를 방지합니다.
Rust에서 One-time 초기화를 구현할 때는 std::sync::Once 구조체를 사용합니다. Once 구조체의 call_once 메서드는 전달된 클로저를 한 번만 실행하도록 보장합니다.
use std::sync::Once;
static INIT: Once = Once::new();
static mut VALUE: u32 = 0;
fn initialize() {
unsafe {
VALUE = 42;
}
}
fn main() {
INIT.call_once(initialize);
println!("VALUE: {}", unsafe { VALUE }); // VALUE: 42
}
위 예제에서 VALUE는 정적 변수이며, initialize 함수는 VALUE를 42로 초기화합니다. INIT.call_once(initialize)를 호출하면, initialize 함수가 한 번만 실행되어 VALUE를 초기화합니다.
One-time 초기화를 활용하면 정적 변수나 전역 변수를 안전하게 초기화할 수 있습니다. 또한, 싱글톤 패턴을 구현할 때도 유용하게 사용될 수 있습니다.
배리어(Barrier)
배리어는 여러 스레드가 특정 지점에서 동기화되도록 하는 동기화 기술입니다. 모든 스레드가 도착할 때까지 기다린 후, 동시에 계속 진행할 수 있습니다.
Rust에서 배리어를 구현할 때는 std::sync::Barrier 구조체를 사용합니다. Barrier는 생성자에 스레드 수를 인자로 받아 초기화됩니다.
use std::sync::Barrier;
use std::thread;
fn main() {
let barrier = Barrier::new(3);
let handle1 = thread::spawn(|| {
println!("스레드 1 준비됨!");
barrier.wait();
println!("스레드 1 계속 실행 중...");
});
let handle2 = thread::spawn(|| {
println!("스레드 2 준비됨!");
barrier.wait();
println!("스레드 2 계속 실행 중...");
});
println!("메인 스레드 준비됨!");
barrier.wait();
println!("메인 스레드 계속 실행 중...");
handle1.join().unwrap();
handle2.join().unwrap();
}
위 예제에서는 Barrier::new(3)을 사용하여 3개의 스레드(메인 스레드와 2개의 자식 스레드)가 동기화되도록 합니다. 각 스레드는 barrier.wait()를 호출하여 다른 스레드가 도착할 때까지 기다린 후, 모든 스레드가 도착하면 계속 실행됩니다.
배리어는 병렬 프로그래밍에서 유용하게 사용됩니다. 예를 들어, 데이터를 분할하여 여러 스레드에서 처리한 후, 결과를 병합할 때 배리어를 사용할 수 있습니다. 이를 통해 모든 스레드가 작업을 완료한 후에만 결과를 병합하도록 할 수 있습니다.
배리어를 사용할 때는 다음과 같은 주의사항이 있습니다:
- 스레드 수 일치: 배리어 생성 시 지정한 스레드 수와 실제 대기 중인 스레드 수가 일치해야 합니다. 그렇지 않으면 데드락이 발생할 수 있습니다.
- 단일 사용 원칙: 배리어는 일회성이며, 재사용할 수 없습니다. 배리어를 다시 사용하려면 새로운 배리어를 생성해야 합니다.
One-time 초기화와 배리어는 동시성 프로그래밍에서 중요한 역할을 합니다. One-time 초기화는 전역 변수나 정적 변수를 안전하게 초기화할 수 있게 해주며, 배리어는 여러 스레드를 동기화하여 병렬 처리를 효율적으로 할 수 있게 해줍니다.
추천 서적:
- "Programming Rust" by Jim Blandy 와 Jason Orendorff (Rust 동기화 기술 심화 설명)
- "Rust in Action" by Tim Macnama와 Jared Bodah (실전 예제 포함)
One-time 초기화와 배리어 등의 동기화 기술을 적절히 활용하면 Rust에서 안전하고 효율적인 동시성 프로그래밍을 할 수 있습니다.
참고 자료
- The Rust Programming Language Book의 "Shared-State Concurrency" 장: https://doc.rust-lang.org/book/ch16-03-shared-state.html
- The Rust Programming Language Book의 "Extensible Concurrency with the Sync and Send Traits" 장: https://doc.rust-lang.org/book/ch16-04-extensible-concurrency-sync-and-send.html
- Rust by Example의 "Synchronization" 장: https://doc.rust-lang.org/rust-by-example/sync.html
- Rust 공식 문서의 "std::sync" 모듈: https://doc.rust-lang.org/std/sync/index.html
- "Programming Rust" 서적 (Jim Blandy와 Jason Orendorff 저)
- "Rust in Action" 서적 (Tim Macnama와 Jared Bodah 저)
한 고대 문서 이야기
여기 한 고대 문서가 있습니다. 이 문서는 B.C. 1,500년 부터 A.D 100년까지 약 1,600 여 년 동안 기록되었습니다. 이 문서의 저자는 약 40 명입니다. 이 문서의 고대 사본은 25,000 개가 넘으나, 사본간 오
gospel79.tistory.com
유튜브 프리미엄 월 1만원 할인받고 월 4000원에 이용하는 방법
올해 5월부터 월 8000원 정도이던 유튜브 프리미엄 요금이 15000원 정도로 인상됩니다. 각종 OTT 서비스, ChatGPT 같은 서비스들이 늘어나다보니 이런 거 몇 개만 이용하더라도 월 이용요금이 5만원을
stock79.tistory.com
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
'IT > Rust 기초 완전 정복' 카테고리의 다른 글
Rust 네트워킹 프로그래밍 기초 (23) (1) | 2024.04.25 |
---|---|
Rust 병렬 프로그래밍 기법 (21) (0) | 2024.04.25 |
Rust 메시지 전달과 공유 메모리 (19) (0) | 2024.04.25 |
Rust 스레드 생성과 관리 (18) (0) | 2024.04.25 |
Rust 동시성 프로그래밍 기초 (17) (1) | 2024.04.24 |
댓글