Rust 동시성 프로그래밍에서 흔히 발생하는 문제점
Ethan Miller
Product Engineer · Leapcell

비동기 프로그래밍은 특정 복잡성을 수반하며, Rust에서 비동기를 사용할 때 실수를 저지르기 쉽습니다. 이 글에서는 Rust 비동기 런타임에서 흔히 발생하는 문제점을 다룹니다.
예상치 못한 동기 차단
비동기 코드에서 실수로 동기 차단 작업을 수행하는 것은 주요 함정입니다. 이는 비동기 프로그래밍의 장점을 훼손하고 성능 병목 현상을 일으킵니다. 다음은 몇 가지 일반적인 시나리오입니다.
- 비동기 함수에서 차단 I/O 작업 사용: 예를 들어
async fn
내에서std::fs::File::open
또는std::net::TcpStream::connect
와 같은 표준 차단 함수를 직접 호출하는 경우. - 비동기 클로저 내에서 CPU 집약적인 작업 수행: 비동기 클로저에서 과도한 계산을 실행하면 현재 스레드를 차단하고 다른 비동기 작업의 실행에 영향을 줄 수 있습니다.
- 비동기 코드에서 차단 라이브러리 또는 함수 사용: 일부 라이브러리는 비동기 인터페이스를 제공하지 않을 수 있으며 동기적으로만 호출할 수 있습니다. 비동기 코드에서 이러한 라이브러리를 사용하면 차단이 발생할 수 있습니다.
std::thread::sleep
과 tokio::time::sleep
을 사용하는 것의 차이점을 비교하기 위해 다음 코드를 살펴보십시오.
use tokio::task; use tokio::time::Duration; async fn handle_request() { println!("Start processing request"); // tokio::time::sleep(Duration::from_secs(1)).await; // Correct: use tokio::time::sleep std::thread::sleep(Duration::from_secs(1)); // Incorrect: using std::thread::sleep println!("Request processing completed"); } #[tokio::main(flavor = "current_thread")] // Use tokio::main macro in single-thread mode async fn main() { let start = std::time::Instant::now(); // Launch multiple concurrent tasks let handles = (0..10).map(|_| { task::spawn(handle_request()) }).collect::<Vec<_>>(); // Optionally wait for all tasks to complete for handle in handles { handle.await.unwrap(); } println!("All requests completed, elapsed time: {:?}", start.elapsed()); }
동기 차단 함정을 피하는 방법은 무엇입니까?
- 비동기 라이브러리 및 함수 사용:
tokio
또는async-std
와 같은 런타임에서 제공하는 비동기 I/O, 타이머 및 네트워킹과 같이 비동기 인터페이스를 제공하는 라이브러리를 선호합니다. - CPU 집약적인 작업을 전용 스레드 풀로 오프로드: 비동기 코드에서 과도한 계산이 필요한 경우
tokio::task::spawn_blocking
또는async-std::task::spawn_blocking
을 사용하여 이러한 작업을 별도의 스레드 풀로 이동하여 주 스레드 차단을 방지합니다. - 종속성을 주의 깊게 검토: 타사 라이브러리를 사용할 때 차단 작업을 도입하지 않도록 비동기 인터페이스를 제공하는지 확인합니다.
- 분석 도구 사용: 성능 분석 도구는 비동기 코드에서 차단 작업을 감지하는 데 도움이 될 수 있습니다. 예를 들어
tokio
는console
이라는 도구를 제공합니다.
.await
을 잊어버림
비동기 함수는 Future
를 반환하며, 실제로 실행하고 결과를 검색하려면 .await
을 사용해야 합니다. .await
을 잊어버리면 Future
가 전혀 실행되지 않습니다.
다음 코드를 고려하십시오.
async fn my_async_function() -> i32 { 42 } #[tokio::main] async fn main() { // Incorrect: forgot `.await`, the function will not execute my_async_function(); // Correct let result = my_async_function().await; println!("The result of the correct async operation is: {}", result); }
spawn
남용
경량 태스크를 과도하게 생성하면 태스크 스케줄링 및 컨텍스트 전환으로 인한 오버헤드가 발생하여 실제로 성능이 저하될 수 있습니다.
아래 예에서는 각 숫자에 2를 곱하고 결과를 Vec
에 저장한 다음 Vec
의 요소 수를 출력합니다. 잘못된 접근 방식과 올바른 접근 방식이 모두 설명되어 있습니다.
use async_std::task; async fn process_item(item: i32) -> i32 { // A very simple operation item * 2 } async fn bad_use_of_spawn() { let mut results = Vec::new(); for i in 0..10000 { // Incorrect: spawning a task for each simple operation let handle = task::spawn(process_item(i)); results.push(handle.await); } println!("{:?}", results.len()); } async fn good_use_of_spawn() { let mut results = Vec::new(); for i in 0..10000 { results.push(process_item(i).await); } println!("{:?}", results.len()); } fn main() { task::block_on(async { bad_use_of_spawn().await; good_use_of_spawn().await; }); }
위의 잘못된 예에서는 각 단순 곱셈에 대해 새 태스크가 생성되어 태스크 스케줄링으로 인한 막대한 오버헤드가 발생합니다. 올바른 접근 방식은 추가 오버헤드를 피하면서 비동기 함수를 직접 기다립니다.
진정한 동시성이 필요한 경우에만 spawn
을 사용해야 합니다. CPU 집약적이거나 장기 실행되는 I/O 바운드 태스크의 경우 spawn
이 적합합니다. 매우 가벼운 태스크의 경우 일반적으로 .await
을 직접 사용하는 것이 더 효율적입니다. tokio::task::JoinSet
을 사용하여 여러 태스크를 보다 효과적으로 관리할 수도 있습니다.
결론
Async Rust는 강력하지만 오용하기 쉽습니다. 차단 호출을 피하고 .await
을 잊지 말고 필요한 경우에만 생성하십시오. 주의해서 작성하면 비동기 코드가 빠르고 안정적으로 유지됩니다.
Rust 프로젝트 호스팅을 위한 최고의 선택, Leapcell입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다중 언어 지원
- Node.js, Python, Go 또는 Rust로 개발하십시오.
무제한 프로젝트를 무료로 배포
- 사용량에 대해서만 지불하십시오. 요청도 수수료도 없습니다.
탁월한 비용 효율성
- 유휴 요금 없이 사용한 만큼만 지불하십시오.
- 예: $25는 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI。
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합。
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅。
손쉬운 확장성 및 고성능
- 고도의 동시성을 쉽게 처리하기 위한 자동 확장。
- 운영 오버헤드가 전혀 없으므로 구축에만 집중하십시오。
설명서에서 자세히 알아보십시오!
X에서 팔로우하세요: @LeapcellHQ