-
Futurelock은 하나의 태스크가 여러 Future를 동시에 관리할 때, 그 중 하나가 다른 Future의 리소스를 필요로 하지만 더 이상 폴링되지 않아 발생하는 교착(deadlock) 현상
-
tokio::select! 구문에서 참조된 Future(&mut future) 와 await를 포함한 분기가 함께 사용될 때 쉽게 발생
- 이 문제는 태스크와 Future의 책임 분리 실패에서 비롯되며, 동일 태스크가 두 Future를 모두 기다리지만 한쪽만 폴링하는 구조로 인해 정지 상태에 빠짐
-
FuturesUnordered, bounded channel, Stream 등에서도 유사한 형태로 발생 가능
- 안전한 비동기 설계를 위해 tokio::spawn으로 Future를 별도 태스크로 분리하거나, select 내에서 await 사용을 피하는 것이 핵심
Futurelock의 개념과 예시
- Futurelock은 Future A가 보유한 리소스를 Future B가 필요로 하지만, 두 Future를 담당하는 태스크가 A를 더 이상 폴링하지 않는 상황에서 발생
- 예시 코드에서는 tokio::select! 내에서 &mut future1과 sleep을 동시에 기다리며, sleep이 먼저 완료되면 future1은 여전히 잠금 대기 상태로 남음
- 이후 future3이 같은 락을 요청하지만, 락은 future1에게 할당되어 있고 future1은 폴링되지 않으므로 프로그램이 영구 정지
tokio::select!와 Mutex의 상호작용
-
tokio::sync::Mutex는 공정(fair) 락으로, 대기 순서대로 락을 부여
- 락은 future1에게 전달되지만, 태스크는 이미 future3만 폴링 중이므로 future1은 실행되지 않음
- Mutex는 다음 대기 태스크를 깨우는 역할만 하며, 어떤 Future가 실제로 폴링되는지는 알 수 없음
Futurelock의 일반적 원인
- 태스크 T가 Future F1을 기다리고, F1이 F2에 의존하며, F2가 다시 T의 폴링을 필요로 하는 순환 의존 구조
- 주로 다음 상황에서 발생
-
tokio::select!에서 &mut future 사용 후 다른 분기에서 await 수행
-
FuturesOrdered 또는 FuturesUnordered에서 일부 Future 완료 후 다른 비동기 작업 수행
- 수동 구현된 Future에서 유사한 동작
Streams 및 기타 구조에서의 발생 사례
-
FuturesOrdered나 FuturesUnordered에서 Future를 꺼낸 뒤, 그와 관련된 리소스를 사용하는 다른 Future를 기다릴 때 Futurelock 발생
-
join_all은 모든 Future를 계속 폴링하므로 Futurelock이 발생하지 않음
실제 사례와 디버깅
-
Omicron#9259 사례에서 모든 데이터베이스 접근 Future가 Futurelock에 걸려 HTTP 요청이 무한 대기
-
mpsc 채널 송신이 차단되었지만 수신 측은 비어 있는 상태로 확인되어 원인 파악이 어려움
- 디버깅 시 tokio-console 같은 도구가 도움이 될 수 있으나, 대부분의 경우 원인 추적이 매우 어려움
Futurelock 방지 지침
- 한 태스크가 여러 Future를 폴링할 때, 이미 시작한 Future의 폴링을 중단하지 않도록 주의
- 가능하면 Future를 새로운 태스크로 spawn하여 독립 실행
-
JoinHandle을 tokio::select!에 전달하면 Futurelock 위험 제거
-
tokio::select! 사용 시 주의할 점
-
&mut future와 await를 동시에 사용하지 말 것
- 두 조건이 모두 존재하면 Futurelock 위험이 높음
-
Stream 사용 시 JoinSet을 활용해 각 Future를 별도 태스크로 실행
-
bounded channel의 용량을 늘리는 것은 근본적 해결책이 아님
- 대신 try_send() 사용으로 블로킹 회피 가능
잘못된 회피 패턴
-
채널 용량을 무한히 늘리는 방법은 비현실적이며 부작용(지연, 메모리 증가) 초래
-
Future 간 의존성 제거 시도는 유지보수 중 새 의존성이 생길 수 있어 취약
- 유일하게 안전한 방법은 tokio::spawn을 통한 태스크 분리
향후 개선 및 보안 고려
- Clippy 린트를 통해 tokio::select! 내 &mut future 사용이나 await 포함 시 경고 제공 가능성 제시
- Futurelock은 서비스 거부(DoS) 형태로 악용될 수 있으나, 본질적으로 비정상 동작이므로 예방이 필요