Rust 동시성 프로그래밍 입문 가이드
Takashi Yamamoto
Infrastructure Engineer · Leapcell

동시성 및 병렬성
많은 사람들이 동시성과 병렬성의 개념을 구별하지 못하므로, Rust에서 비동기 프로그래밍을 시작하기 전에 동시성과 병렬성의 차이를 먼저 이해하는 것이 중요합니다.
우리는 종종 운영 체제 교과서에서 다음과 같은 정의를 접합니다.
-
동시성은 동일한 시간 간격 내에서 두 개 이상의 이벤트가 발생하는 것을 의미합니다.
-
병렬성은 시스템이 계산 또는 작업을 동시에 수행할 수 있는 능력을 의미합니다.
-
설명 1: 동시성은 두 개 이상의 이벤트가 동일한 시간 간격 내에서 발생하는 것을 의미하고, 병렬성은 두 개 이상의 이벤트가 정확히 같은 순간에 발생하는 것을 의미합니다.
-
설명 2: 동시성은 동일한 개체에서 여러 이벤트가 발생하는 것을 의미하고, 병렬성은 서로 다른 개체에서 여러 이벤트가 발생하는 것을 의미합니다.
-
설명 3: 동시성은 단일 프로세서에서 여러 작업을 "동시"에 처리하는 것이고, 병렬성은 분산 클러스터와 같이 여러 프로세서에서 작업을 동시에 처리하는 것입니다.
Golang의 창시자 중 한 명인 Rob Pike는 매우 통찰력 있고 직관적인 설명을 제공했습니다.
동시성은 한 번에 많은 것을 처리하는 것에 관한 것입니다. 병렬성은 한 번에 많은 것을 수행하는 것에 관한 것입니다.
동시성은 한 번에 많은 작업을 처리할 수 있는 능력이고, 병렬성은 한 번에 많은 작업을 실행하는 기술입니다.
작업을 여러 스레드 또는 비동기 작업에 배치하여 처리하는 경우 동시성을 활용하는 것입니다. 이러한 스레드 또는 비동기 작업이 멀티 코어 또는 멀티 CPU 머신에서 동시에 실행되면 병렬성을 활용하는 것입니다. 어떤 의미에서 동시성은 병렬성을 가능하게 합니다. 동시 작업을 처리할 수 있는 능력이 있으면 병렬성이 자연스럽게 따라옵니다.
- 동시성: 주어진 순간에 하나의 명령어만 실행되지만, 여러 프로세스 명령어가 빠르게 전환되어 매크로적으로 동시에 실행되는 것처럼 보이는 것을 의미합니다. 그러나 마이크로 수준에서는 실제로 동시에 실행되지 않습니다. 시간을 분할하여 프로세스 간에 빠르게 번갈아 실행할 수 있도록 합니다.
- 병렬성: 서로 다른 프로세서에서 여러 명령어가 동시에 실행되는 것을 의미합니다. 따라서 마이크로 및 매크로 수준 모두에서 작업이 동시에 실행되고 처리됩니다.
여러 스레드가 작동 중이고 시스템에 CPU가 하나만 있는 경우 둘 이상의 스레드가 실제로 동시에 실행되는 것은 불가능합니다. 시스템은 CPU 시간을 분할하여 스레드에 할당합니다. 한 스레드가 실행 중일 때 다른 스레드는 일시 중단됩니다. 이 접근 방식을 동시성이라고 합니다.
시스템에 CPU가 두 개 이상 있는 경우 스레드 작업이 동시적이지 않을 수 있습니다. 한 CPU가 한 스레드를 실행하는 동안 다른 CPU는 다른 스레드를 실행할 수 있습니다. 스레드는 동일한 CPU 리소스를 놓고 경쟁하지 않으며 동시에 실행될 수 있습니다. 이를 병렬성이라고 합니다.
결론은 다음과 같습니다. 동시성과 병렬성은 모두 멀티태스킹을 설명합니다. 동시성은 교대 실행에 관한 것이며 동시성 수준과 같은 처리 용량에 중점을 둡니다. 병렬성은 동시 실행에 관한 것이며 작업 병렬 처리와 같은 실행 전략에 중점을 둡니다.
동시성 프로그래밍 모델
프로그래밍 언어마다 구현이 다르기 때문에 언어 간에 다양한 동시성 모델이 존재한다는 것을 알고 있습니다. 특정 언어로 프로그램을 작성하고 컴파일하면 해당 프로그램은 실행될 때 프로세스를 차지합니다. 이 프로세스 내에서 스레드를 생성할 수 있으며, 이는 운영 체제 수준에 있습니다. 언어 자체 내에서 프로그래머가 언어 수준 기능을 사용하여 생성한 스레드는 언어 수준 스레드로 간주됩니다. 이러한 두 유형의 스레드가 일대일인지 여부는 언어의 내부 구현에 따라 다릅니다.
- OS-네이티브 스레드: 예를 들어 Rust 언어는 운영 체제에서 제공하는 API를 직접 호출하므로 프로그램의 스레드 수는 사용된 OS 스레드 수와 일치합니다.
- 코루틴: Go 언어가 작동하는 방식과 유사합니다. 내부적으로 프로그램의 M 스레드는 어떤 방식으로든 N 운영 체제 스레드에 매핑됩니다.
- 이벤트 기반: 이 모델은 콜백과 함께 자주 사용됩니다. 성능 면에서 매우 뛰어나지만 가장 큰 문제는 _콜백 지옥_의 위험입니다.
- 액터 모델: 메시지 전달을 기반으로 하는 이 모델은 분해된 작은 단위에서 동시 계산을 수행합니다. Erlang 언어의 킬러 기능입니다.
- Async/await 모델: 이 모델은 고성능을 제공하고, 저수준 프로그래밍을 지원하며, 프로그래밍 모델에 큰 변화를 주지 않고도 스레드 또는 코루틴과 유사하게 동작합니다. 그러나 절충점은 내부 구현 메커니즘이 상당히 복잡하다는 것입니다.
요약하자면, Rust는 장단점을 고려한 후 멀티 스레딩과 async/await를 두 가지 동시성 프로그래밍 모델로 제공하기로 최종 결정했습니다.
- 멀티 스레딩은 OS API를 직접 호출하여 표준 라이브러리에서 구현됩니다. 구현 및 사용이 간단하여 소수의 동시성 작업이 필요한 시나리오에 적합합니다.
- Async/await는 구현이 더 복잡하지만 Rust는 언어 기능, 표준 라이브러리 및 타사 라이브러리의 조합을 사용하여 추상화하고 캡슐화합니다. 이를 통해 개발자는 기본 구현 로직에 대해 걱정하지 않고도 async/await를 사용할 수 있습니다. 대규모 동시성 및 비동기 I/O에 적합합니다.
Rust의 비동기 프로그래밍
비동기 프로그래밍은 동시성 프로그래밍 모델입니다. 이를 통해 몇 개 또는 심지어 단일 OS 스레드 또는 CPU 코어만 사용하여 많은 수의 작업을 동시에 실행할 수 있습니다. 사용 경험 측면에서 최신 비동기 프로그래밍은 동기 프로그래밍과 거의 구별할 수 없습니다.
오늘날 많은 언어가 'async'를 통해 비동기 프로그래밍을 지원하지만, Rust의 구현은 여러 가지 중요한 측면에서 다릅니다:
- Rust에서 Futures는 지연됩니다: _폴링_될 때만 실행되기 시작합니다. future를 폐기하면 실행되지 않습니다.
Future
는 미래의 특정 시점에 실행되도록 예약된 작업이라고 생각할 수 있습니다. - 제로 코스트 추상화: Rust에서
async
를 사용하면 런타임 비용이 0입니다. 즉, 작성한 코드(눈에 보이는 코드)만 성능 오버헤드가 발생합니다.async
의 내부 구현은 성능 저하를 유발하지 않습니다. 예를 들어async
를 사용하기 위해 힙 메모리를 할당하거나 동적 디스패치를 수행할 필요가 없습니다. 이는 특히 핫 경로에서 성능에 큰 도움이 되며 Rust의 비동기 성능이 매우 높은 이유 중 하나입니다. - Rust에는 async 호출에 필요한 기본 제공 런타임이 포함되어 있지 않지만 걱정할 필요는 없습니다. Rust의 생태계는 잘 알려진 Tokio와 같은 훌륭한 런타임 구현을 제공합니다.
- 런타임은 단일 스레드 및 다중 스레드 모드를 모두 지원하며, 각 모드에는 고유한 장단점이 있으며 나중에 설명합니다.
Async와 멀티스레딩 중에서 선택하기
async
와 멀티스레딩은 모두 동시성 프로그래밍을 달성하는 데 사용할 수 있으며, 후자는 스레드 풀을 통해 동시성을 향상시킬 수도 있지만 이러한 두 가지 접근 방식은 상호 교환할 수 없습니다. 하나에서 다른 하나로 전환하려면 상당한 코드 리팩터링이 필요한 경우가 많습니다. 따라서 차이점과 적용 가능한 시나리오를 이해하고 미리 올바른 선택을 하는 것이 매우 중요합니다.
- 병렬 계산과 같은 CPU 집약적인 작업의 경우 멀티스레딩이 더 유리합니다. 이러한 작업은 스레드를 장시간 동안 최대 용량으로 실행하는 경향이 있기 때문입니다. 생성하는 스레드 수는 병렬 처리 기능을 최대한 활용하기 위해 CPU 코어 수와 같아야 합니다. 이 경우 스레드 컨텍스트 전환은 성능 오버헤드를 유발하기 때문에 스레드를 자주 생성하거나 전환할 필요가 없습니다. 스레드를 특정 CPU 코어에 바인딩하여 이러한 오버헤드를 줄일 수 있습니다.
- 웹 서버, 데이터베이스 연결 및 기타 네트워크 서비스와 같은 IO 집약적인 작업의 경우 비동기 프로그래밍이 더 유리합니다. 이러한 작업은 대부분의 시간을 대기하는 데 소비하기 때문입니다. 멀티스레딩을 사용하는 경우 대부분의 스레드가 대부분의 시간 동안 유휴 상태로 유지됩니다. 높은 스레드 컨텍스트 전환 비용과 결합되어 상당한 성능 손실이 발생합니다.
async
를 사용하면 CPU 및 메모리 사용량을 효과적으로 줄이면서 여전히 많은 수의 작업을 동시에 실행할 수 있습니다. 작업이 IO 또는 기타 차단 상태로 들어가면 즉시 양보하여 다른 작업을 실행할 수 있습니다.async
에서 작업을 전환하는 데 드는 비용은 멀티스레딩에서 스레드 컨텍스트를 전환하는 데 드는 비용보다 훨씬 낮습니다.
참고: Async는 내부적으로 스레드를 기반으로 합니다. 그러나 런타임을 통해 스레드를 래핑하여 여러 작업을 적은 수의 스레드에 매핑합니다. 기본적으로 많은 수의 IO 바운드 동시 이벤트를 적은 수의 스레드에 던져서 이벤트를 통해 효율적으로 통신합니다.
이 접근 방식의 비용은 Rust 프로그램의 런타임을 증가시킨다는 것입니다(런타임은 모든 실행 파일에 번들로 제공되는 Rust 코드입니다). 이로 인해 컴파일된 바이너리 크기가 크게 증가합니다.
두 가지의 차이점을 간단한 예로 설명해 보겠습니다. 두 개의 파일을 다운로드하려고 한다고 가정합니다. 파일을 하나씩 다운로드할 수 있지만 이는 분명히 가장 빠른 방법이 아닙니다. 당연히 멀티스레딩을 사용하여 병렬 다운로드를 생각합니다.
멀티스레드 프로그래밍:
fn download_two_files() { // 작업을 수행할 두 개의 새 스레드를 만듭니다. let thread_one = thread::spawn(|| download("URL1")); let thread_two = thread::spawn(|| download("URL2")); // 두 스레드가 모두 완료될 때까지 기다립니다. thread_one.join().expect("thread one panic"); thread_two.join().expect("thread two panic"); }
매번 한두 개의 파일만 다운로드하는 경우 이 접근 방식은 잘 작동합니다. 그러나 수백 또는 수천 개의 파일을 동시에 다운로드해야 하는 경우 문제가 발생합니다. 각 다운로드 작업은 스레드를 소비하고 스레드의 리소스 비용이 빠르게 확장됩니다(스레드는 여전히 너무 무겁습니다). 이 경우 async
를 사용하는 것이 좋습니다.
Async 프로그래밍:
async fn get_two_sites_async() { // 두 개의 별도 future를 만듭니다. // future는 미래의 특정 시점에 실행되도록 예약된 작업이라고 생각할 수 있습니다. JS의 Promise와 유사합니다. // 두 future가 모두 실행되면 대상 페이지를 동시에 다운로드합니다. let future_one = download_async("URL1"); let future_two = download_async("URL2"); // 완료될 때까지 두 future를 동시에 실행합니다. join!(future_one, future_two); }
멀티스레드 모델과 비교하여 async는 여기에서 장점을 보여줍니다. 동일한 수준의 동시성에 대해 스레드를 만들고 전환하는 데 드는 비용을 줄입니다.
요약
동시성과 병렬성은 둘 다 다중 작업 처리에 대한 설명입니다. 동시성은 작업을 차례로 처리하는 것을 의미하고, 병렬성은 작업을 동시에 처리하는 것을 의미합니다. 동시 프로그래밍은 프로그램의 서로 다른 부분이 독립적으로 실행되는 것을 의미하고, 병렬 프로그래밍은 프로그램의 서로 다른 부분이 동시에 실행되는 것을 의미합니다.
동시성 프로그래밍 모델 측면에서 Rust의 언어 설계 철학, 즉 안전성, 성능 및 제어를 강조하기 때문에 Rust는 Go와 같은 "급진적인 단순성" 접근 방식을 채택하지 않았습니다. 대신 멀티스레딩과 async/await를 결합하기로 선택했습니다. 이것의 장점은 더 강력한 제어와 더 높은 성능입니다. 단점은 복잡성이 더 높다는 것입니다. 그러나 물론 이것은 시스템 프로그래밍 언어에 대한 예상되는 절충점입니다. 제어 및 성능을 위해 복잡성을 사용하는 것입니다.
사실 async와 멀티스레딩은 상호 배타적이지 않습니다. 많은 애플리케이션에서 둘 다 함께 사용됩니다. async
와 멀티스레딩은 모두 동시성 프로그래밍을 달성할 수 있으며, 멀티스레딩은 스레드 풀을 활용하여 동시성을 향상시킬 수도 있지만 이러한 두 모델은 상호 운용할 수 없습니다. 하나에서 다른 하나로 전환하려면 대규모 코드 리팩터링이 필요합니다. 따라서 프로젝트 초기에 올바른 동시성 모델을 선택하는 것이 매우 중요합니다.
결론적으로 async 프로그래밍은 IO 바운드 작업에 적합하고, 멀티스레딩은 CPU 바운드 작업에 적합합니다. 다음은 선택 규칙에 대한 간략한 요약입니다.
- 동시에 실행해야 하는 많은 IO 작업이 있는 경우 async 모델을 선택합니다.
- 동시에 실행해야 하는 몇 가지 IO 작업이 있는 경우 멀티스레딩을 선택합니다. 스레드 생성 및 삭제 오버헤드를 줄이려면 스레드 풀을 사용할 수 있습니다.
- 병렬로 실행해야 하는 많은 CPU 집약적인 작업(예: 과도한 계산)이 있는 경우 멀티스레딩 모델을 선택하고 스레드 수를 CPU 코어 수와 일치시키거나 약간 초과하도록 시도합니다.
- 선택이 실제로 중요하지 않은 경우 기본적으로 멀티스레딩을 사용합니다.
Rust 프로젝트 호스팅을 위한 최고의 선택인 Leapcell입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다국어 지원
- Node.js, Python, Go 또는 Rust로 개발하세요.
무제한 프로젝트를 무료로 배포
- 사용량에 대해서만 비용을 지불하세요. 요청이나 요금이 없습니다.
탁월한 비용 효율성
- 유휴 요금 없이 사용한 만큼 지불하세요.
- 예: $25는 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI입니다.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합입니다.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅입니다.
손쉬운 확장성 및 고성능
- 높은 동시성을 쉽게 처리할 수 있도록 자동 확장합니다.
- 운영 오버헤드가 없으므로 구축에만 집중하세요.
자세한 내용은 설명서에서 확인하세요!
X에서 팔로우하세요: @LeapcellHQ