Rust에서 멀티스레드 향상: 심층적인 Arc 최적화
Grace Collins
Solutions Engineer · Leapcell

Rust 프로그래밍에서 Arc(원자 참조 카운팅)와 뮤텍스(예: Mutex)를 결합하는 것은 멀티스레드 환경에서 데이터를 공유하고 수정하는 데 사용되는 일반적인 패턴입니다. 그러나 이 접근 방식은 특히 높은 잠금 경합 상태에서 성능 병목 현상을 일으킬 수 있습니다. 이 기사에서는 잠금 경합을 줄이고 스레드 안전성을 유지하면서 성능을 향상시키는 여러 최적화 기술을 살펴봅니다. 예를 들어 다음 사례를 고려해 보겠습니다.
세분화된 잠금 사용
성능을 향상시키는 한 가지 방법은 더 세분화된 잠금을 사용하는 것입니다. 이는 데이터 구조를 여러 부분으로 분해하고 각 부분에 자체 잠금 메커니즘을 갖도록 하여 달성할 수 있습니다. 예를 들어 Mutex를 RwLock으로 교체하면 읽기 작업이 쓰기 작업보다 훨씬 많은 경우 효율성을 높일 수 있습니다. 샘플 코드는 데이터 구조 T의 각 부분을 자체 RwLock에 배치하여 이러한 부분을 독립적으로 잠그고 잠금 해제할 수 있는 방법을 보여줍니다.
use std::sync::{Arc, RwLock}; use std::thread; // T가 두 부분으로 구성된 복잡한 데이터 구조라고 가정합니다. struct T { part1: i32, part2: i32, } // T의 각 부분을 자체 RwLock에 배치합니다. struct SharedData { part1: RwLock<i32>, part2: RwLock<i32>, } // 이 함수는 데이터에 대한 빈번한 액세스 및 수정을 시뮬레이션합니다. fn frequent_access(data: Arc<SharedData>) { { // 수정해야 하는 부분만 잠급니다. let mut part1 = data.part1.write().unwrap(); *part1 += 1; // part1 수정 } // part1에 대한 잠금이 여기서 해제됩니다. // 다른 부분은 동시에 읽거나 쓸 수 있습니다. // ... } fn main() { let data = Arc::new(SharedData { part1: RwLock::new(0), part2: RwLock::new(0), }); // 공유 데이터 액세스를 보여주기 위해 여러 스레드를 만듭니다. let mut handles = vec![]; for _ in 0..10 { let data_clone = Arc::clone(&data); let handle = thread::spawn(move || { frequent_access(data_clone); }); handles.push(handle); } // 모든 스레드가 완료될 때까지 기다립니다. for handle in handles { handle.join().unwrap(); } println!("Final values: Part1 = {}, Part2 = {}", data.part1.read().unwrap(), data.part2.read().unwrap()); }
이 예에서는 std::sync::RwLock
을 사용하여 더 세분화된 잠금을 구현합니다. RwLock은 여러 판독기 또는 한 명의 기록기를 허용하며 읽기 작업이 쓰기 작업보다 훨씬 많은 시나리오에서 매우 유용합니다. 이 예에서는 T의 각 부분이 자체 RwLock에 배치됩니다. 이를 통해 이러한 부분을 독립적으로 잠글 수 있으므로 스레드 안전성을 희생하지 않고 성능을 향상시킬 수 있습니다. 한 부분을 수정하는 동안에는 해당 부분의 잠금만 유지되고 다른 부분은 다른 스레드에서 읽거나 쓸 수 있습니다.
이 방법은 데이터 구조를 비교적 독립적인 부분으로 명확하게 분해할 수 있는 상황에 적합합니다. 이러한 시스템을 설계할 때는 데이터 일관성과 교착 상태의 위험을 신중하게 고려해야 합니다.
데이터 복제 및 잠금 지연
또 다른 방법은 데이터를 수정하기 전에 복제하고 공유 데이터를 업데이트할 때만 잠그는 것입니다. 이 접근 방식은 뮤텍스가 유지되는 시간을 줄여 성능을 향상시킵니다. 이 방법에서는 잠금 외부에서 데이터를 복제한 다음 잠금 없이 복사본을 수정합니다. 공유 데이터를 업데이트해야 할 때만 업데이트를 위해 잠금을 다시 획득합니다. 이렇게 하면 잠금 유지 시간이 줄어들어 다른 스레드가 공유 리소스에 더 빠르게 액세스할 수 있습니다.
use std::sync::{Arc, Mutex}; use std::thread; // T가 복제할 수 있는 복잡한 데이터 구조라고 가정합니다. #[derive(Clone)] struct T { value: i32, } // 이 함수는 데이터에 대한 빈번한 액세스 및 수정을 시뮬레이션합니다. fn frequent_access(data: Arc<Mutex<T>>) { // 잠금 외부에서 데이터를 복제합니다. let mut data_clone = { let data_locked = data.lock().unwrap(); data_locked.clone() }; // 복제된 데이터를 수정합니다. data_clone.value += 1; // 공유 데이터를 업데이트할 때만 뮤텍스를 잠급니다. let mut data_shared = data.lock().unwrap(); *data_shared = data_clone; } fn main() { let data = Arc::new(Mutex::new(T { value: 0 })); // 공유 데이터 액세스를 보여주기 위해 여러 스레드를 만듭니다. let mut handles = vec![]; for _ in 0..10 { let data_clone = Arc::clone(&data); let handle = thread::spawn(move || { frequent_access(data_clone); }); handles.push(handle); } // 모든 스레드가 완료될 때까지 기다립니다. for handle in handles { handle.join().unwrap(); } println!("Final value: {}", data.lock().unwrap().value); }
이 코드의 목적은 뮤텍스(Mutex)가 유지되는 시간을 줄여 성능을 향상시키는 것입니다. 이 프로세스를 단계별로 분석해 보겠습니다.
-
잠금 외부에서 데이터 복제:
let mut data_clone = { let data_locked = data.lock().unwrap(); data_locked.clone() };
여기서는 먼저
data.lock().unwrap()
을 사용하여data
에서 잠금을 획득하고 즉시 데이터를 복제합니다. 복제 작업이 완료되면 블록({})의 범위가 끝나고 잠금이 자동으로 해제됩니다. 즉, 복제된 데이터에 대해 작업하는 동안에는 원래 데이터가 잠기지 않습니다. -
복제된 데이터 수정:
data_clone.value += 1;
data_clone
은data
의 복사본이므로 잠금 없이 자유롭게 수정할 수 있습니다. 이것이 성능 향상의 핵심입니다. 잠재적으로 시간이 오래 걸리는 데이터 수정 중에 잠금을 유지하지 않으므로 다른 스레드가 잠금을 기다리는 동안 차단되는 시간을 줄입니다. -
공유 데이터를 업데이트할 때만 뮤텍스 잠금:
let mut data_shared = data.lock().unwrap(); *data_shared = data_clone;
수정이 완료되면
data
에서 잠금을 다시 획득하고 수정된data_clone
으로 업데이트합니다. 이 단계는 공유 데이터에 대한 업데이트가 스레드 안전하도록 보장하는 데 필요합니다. 중요한 점은 잠금이 이 짧은 업데이트 단계 동안에만 유지된다는 것입니다.
잠금 유지 시간을 줄임으로써 이 접근 방식은 특히 잠금 경합이 심할 때 멀티스레드 환경에서 성능에 매우 중요합니다. 잠금 유지 시간이 짧을수록 다른 스레드가 공유 리소스에 더 빠르게 액세스할 수 있으므로 애플리케이션의 전반적인 응답성과 처리량이 향상됩니다.
그러나 이 방법은 또한 메모리 사용량을 늘리고(데이터를 복제해야 하므로) 더 복잡한 동기화 로직을 도입할 수 있다는 단점이 있습니다. 따라서 이 방법을 사용할지 여부를 결정할 때는 특정 상황에 따라 장단점을 따져보는 것이 중요합니다.
저희 Leapcell은 Rust 프로젝트 호스팅을 위한 최고의 선택입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다국어 지원
- Node.js, Python, Go 또는 Rust로 개발하세요.
무제한 프로젝트를 무료로 배포
- 사용량에 대해서만 비용을 지불하세요. 요청도 없고 요금도 없습니다.
압도적인 비용 효율성
- 유휴 요금 없이 사용한 만큼 지불하세요.
- 예: $25로 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅
간편한 확장성 및 고성능
- 높은 동시성을 쉽게 처리하기 위한 자동 스케일링
- 운영 오버헤드가 전혀 없으므로 구축에만 집중하세요.
설명서에서 자세히 알아보세요!
X에서 저희를 팔로우하세요: @LeapcellHQ