비동기 인식 프리미티브 (Async-aware Primitives)
tokio 문서를 살펴보면 표준 라이브러리의 타입들과 이름은 같지만 비동기적인 특성을 가진 타입들을 많이 발견할 수 있습니다. 예를 들어 잠금(Locks), 채널(Channels), 타이머(Timers) 등이 있죠.
비동기 컨텍스트에서 작업할 때는 동기 방식의 대응물보다 이런 비동기 대안들을 우선적으로 사용해야 합니다.
왜 그런지 이해하기 위해, 이전 챕터에서 다뤘던 상호 배제 잠금인 Mutex를 예로 들어보겠습니다.
사례 연구: Mutex
간단한 예제 코드를 보겠습니다:
use std::sync::{Arc, Mutex};
async fn run(m: Arc<Mutex<Vec<u64>>>) {
let guard = m.lock().unwrap();
http_call(&guard).await;
println!("Sent {:?} to the server", &guard);
// `guard`는 여기서 드롭(Drop)됩니다.
}
/// `v`를 HTTP 호출의 본문으로 사용합니다.
async fn http_call(v: &[u64]) {
// [...]
}
std::sync::MutexGuard와 양보 지점
이 코드는 컴파일은 되지만 매우 위험합니다.
비동기 컨텍스트 내에서 std 라이브러리의 Mutex 잠금을 획득하고 있습니다. 그리고 그 결과물인 MutexGuard를 유지한 채로 양보 지점(http_call의 .await)을 넘어갑니다.
단일 스레드 런타임에서 두 개의 태스크가 동시에 run을 실행한다고 가정해 봅시다. 스케줄링 순서는 다음과 같을 수 있습니다:
태스크 A 태스크 B
|
잠금 획득
런타임에 양보
|
+--------------+
|
잠금 획득 시도 (차단됨!)
여기서 **교착 상태(Deadlock)**가 발생합니다. 태스크 B는 잠금을 얻으려 하지만, 이미 태스크 A가 잠금을 쥐고 있는 상태입니다. 태스크 A는 잠금을 해제하기 전 런타임에 제어권을 양보했는데, 런타임은 태스크 B를 강제로 중단(선점)할 수 없으므로 태스크 A가 다시 실행될 기회를 얻지 못하게 됩니다.
tokio::sync::Mutex
이 문제는 tokio::sync::Mutex로 바꾸면 깔끔하게 해결됩니다:
use std::sync::Arc;
use tokio::sync::Mutex;
async fn run(m: Arc<Mutex<Vec<u64>>>) {
let guard = m.lock().await;
http_call(&guard).await;
println!("Sent {:?} to the server", &guard);
// `guard`는 여기서 드롭됩니다.
}
이제 잠금을 획득하는 과정 자체가 비동기 작업이 됩니다. 만약 잠금을 즉시 얻을 수 없다면 런타임에 제어권을 양보하죠. 다시 이전 시나리오를 적용해 보면 다음과 같이 동작합니다:
태스크 A 태스크 B
|
잠금 획득
`http_call` 시작
런타임에 양보
|
+--------------+
|
잠금 획득 시도
(획득 불가 -> 런타임에 양보)
|
+--------------+
|
`http_call` 완료
잠금 해제
런타임에 양보
|
+--------------+
|
잠금 획득 성공!
[...]
모든 것이 정상적으로 동작하네요!
다중 스레드라도 안전하지 않습니다
위 예제에서는 단일 스레드 런타임을 가정했지만, 다중 스레드 런타임에서도 위험은 여전합니다. 차이가 있다면 교착 상태를 일으키는 데 필요한 태스크의 수입니다. 단일 스레드에서는 2개면 충분하지만, 다중 스레드에서는 실행 스레드 수 N에 대해 N+1개의 태스크가 필요할 뿐입니다.
트레이드오프
비동기 인식 Mutex를 사용하는 것은 약간의 성능 오버헤드가 따릅니다. 만약 잠금 경합이 거의 발생하지 않고, .await 지점을 넘어서 잠금을 유지하지 않는다는 확신이 있다면 비동기 컨텍스트에서도 std::sync::Mutex를 계속 사용할 수 있습니다.
하지만 얻을 수 있는 성능 이점이 잠재적인 교착 상태의 위험보다 큰지 항상 신중하게 고민해야 합니다.
다른 프리미티브들
Mutex 외에도 RwLock, 세마포어(Semaphore) 등 다른 도구들에도 동일한 원리가 적용됩니다. 문제를 미연에 방지하려면 비동기 컨텍스트에서 작업할 때는 언제나 비동기 인식 버전을 우선적으로 고려하세요.
Exercise
The exercise for this section is located in 08_futures/06_async_aware_primitives