다른 언어와 비교한 Rust의 동시성(concurrency) 장,단점

rust의 동시성

Rust의 동시성(concurrency)은 안전성과 성능을 모두 고려한 설계로, 데이터 경쟁(data race)을 컴파일 타임에 방지하고, 성능 저하 없이 병렬 처리를 가능하게 합니다. C++, Java, Python, Go 등 타 언어와 비교해 장단점을 알아보겠습니다.

1. Rust의 동시성 개념

Rust는 동시성을 다음 세 가지 방식으로 지원합니다:

  1. 스레드 기반 동시성 (std::thread)
    • OS 스레드를 생성하여 병렬 작업 수행.
    • thread:spawn을 통해 새로운 스레드를 실행.
  2. 메시지 기반 통신 (std::sync::mpsc)
    • 채널을 통해 스레드 간 데이터 교환.
    • 데이터 소유권을 안전하게 이동.
  3. 비동기 프로그래밍 (async/await, tokio, async-std)
    • 효율적인 I/O 처리.
    • 싱글 스레드에서 수천 개의 작업을 동시에 처리할 수 있음.
    • Future를이용한 논블로킹 방식.

2. Rust 동시성의 핵심 특징

특징설명
데이터 레이스 방지컴파일 타임에 mut, &mut, Send, Sync 등을 통해 공유 자원에 대한 안전성 확보
제로 코스트 추상화고급 추상화를 사용해도 런타임 오버헤드 없음
fearless concurrency안전하게 동시성을 구현할 수 있어 “두려움 없는 동시성”이라고도 불림

3. 타 언어와의 비교

가. Rust vs C++

항목RustC++
안전성컴파일 타임 데이터 레이스 방지런타임에서 버그 발견 가능
메모리 모델소유권 시스템수동 메모리 관리
쓰레드 API안전하고 모던한 추상화복잡하고 안전하지 않은 경우 많음

🔹 Rust는 안전하고 버그 없는 병렬 처리를 제공
🔸 C++은 성능은 뛰어나지만 관리 책임이 개발자에게 있음 (예: 뮤텍스 실수 → 데이터 손상)

나. Rust vs Java

항목RustJava
런타임없음 (네이티브 실행)JVM 기반
동기화Mutex, RwLock, channel 등 명시적synchronized, volatile, ExecutorService 등
성능시스템 수준 고성능GC와 JVM 오버헤드 존재

🔹 Rust는 GC 없는 고성능 동시성
🔸 Java는 GC로 메모리 관리가 쉽지만 지연 가능성 존재

다. Rust vs Python

항목RustPython
성능매우 빠름느림 (인터프리터 기반)
GIL (Global Interpreter Lock)없음있음 (멀티 코어 병렬 처리 불가)
비동기 처리고성능 async/awaitasyncio로 가능하나 성능은 낮음

🔹 Rust는 진짜 병렬 처리 가능
🔸 Python은 GIL 때문에 CPU 병렬처리에 약함 (I/O 병렬만 현실적)

라. 요약

구분Rust의 장점Rust의 단점
성능네이티브 수준의 성능안전성을 위한 빌드 시간 증가
안전성데이터 레이스를 컴파일 타임에 방지초기 진입 장벽 (개념이 복잡함)
표현력async/await, channel, Mutex 등 현대적 추상화도구/라이브러리 생태계가 다른 언어보다 적은 편
병렬성GIL 없음, 진짜 병렬 처리 가능쓰레드 디버깅이 어려울 수 있음

4. Rust 동시성이 특히 유리한 분야

  • 고성능 웹 서버 (예: Actix, Axum)
  • 실시간 시스템 (예: 게임, IoT)
  • 병렬 데이터 처리 (예: 이미지/영상 처리)
  • 시스템 프로그래밍 (드라이버, 임베디드)

5. 1부터 100만까지 숫자의 합을 4개 스레드로 나눠 병렬 계산 비교

가. Rust 버전

use std::thread;

fn main() {
let data: Vec<u64> = (1..=1_000_000).collect();
let chunk_size = data.len() / 4;

let mut handles = vec![];

for i in 0..4 {
let chunk = data[i * chunk_size..(i + 1) * chunk_size].to_vec();
let handle = thread::spawn(move || chunk.iter().sum::<u64>());
handles.push(handle);
}

let total: u64 = handles.into_iter().map(|h| h.join().unwrap()).sum();
println!("합계: {}", total);
}

설명:

use std::thread;
  • Rust의 표준 라이브러리에서 thread 모듈을 가져옵니다. 병렬 처리를 위해 사용됩니다.
let data: Vec<u64> = (1..=1_000_000).collect();
  • (1..=1_000_000)는 표현식이며 RangeInclusive 타입으로, 1부터 1_000_000까지 포함하는 이터레이터입니다.
  • .collect()는 이터레이터(iterator)를 모아서 컬렉션(예: Vec, HashMap, String)으로 변환하는 메서드입니다.
  • 명시적으로 Vec 타입을 선언했기 때문에, collect()는 모든 숫자를 벡터로 수집하게 됩니다.
let chunk_size = data.len() / 4;
  • 데이터를 4개의 스레드로 나눌 것이기 때문에, 각 스레드가 처리할 데이터의 크기를 계산합니다.
  • chunk_size = 250_000
let mut handles = vec![];
  • 스레드 핸들(JoinHandle)들을 저장할 벡터.
  • 각 스레드는 나중에 .join()으로 결과를 수집할 수 있습니다.
for i in 0..4 {
let chunk = data[i * chunk_size..(i + 1) * chunk_size].to_vec();
let handle = thread::spawn(move || chunk.iter().sum::<u64>());
handles.push(handle);
}
  • 루프를 4번 돌며 벡터를 4등분합니다.
let chunk = data[i * chunk_size..(i + 1) * chunk_size].to_vec();
  • data를 4등분하여 각 스레드에 넘길 복사본을 만든 후 chunk에 할당합니다.
  • i가 0~3까지 반복되므로:
    • i = 0 일 때는 data[0..250000]
    • i = 1 일 때는 data[250000..500000]
    • i = 2 일 때는 data[500000..750000]
    • i = 3 일 때는 data[750000..1000000]
  • 이렇게 전체 데이터를 4개의 슬라이스(slice)로 나눕니다. 하지만 슬라이스는 참조(&)이며, 여러 스레드가 같은 데이터를 공유할 때, 데이터 경합(data race)을 막기 위해 컴파일러가 참조의 안전성을 보장해야 하므로, .to_vec()을 사용하여 슬라이스의 복사본을 만들어 소유권을 가지는 새 벡터로 만듭니다. 이제 이 벡터는 독립적 소유권을 가지므로, move를 통해 클로저에 안전하게 넘길 수 있습니다
let handle = thread::spawn(move || chunk.iter().sum::<u64>());
  • thread::spawn(…) → 새로운 스레드(thread)를 만들어서 주어진 작업을 실행합니다.
  • move || … → 클로저(익명 함수)에서 외부 변수인 chunk의 소유권을 이동시켜 사용합니다.
  • chunk.iter().sum::() → chunk의 모든 원소를 합산하여 u64값을 반환합니다.
  • 반환된 handle은 JoinHandle 타입이고, 이걸 handles 벡터에 저장해 나중에 결과를 수집합니다.
handles.push(handle);
  • thread::spawn(…)의 결과인 JoinHandle을 handles 벡터에 저장합니다.
  • 이 handles는 모든 스레드 작업이 끝난 뒤 결과를 수집하는 데 사용됩니다.
let total: u64 = handles.into_iter().map(|h| h.join().unwrap()).sum();
  • 스레드에서 계산한 4개의 부분합을 모아서 전체 합을 계산한다.
  • handles.into_iter() .into_iter()는 handles 벡터의 소유권을 consuming iterator로 가져옵니다(copy가 아닌 move). 즉, 이후 handles는 더 이상 사용할 수 없습니다.
  • .map(|h| h.join().unwrap())의각 h는 JoinHandle 이고, h.join()은 이 스레드가 끝날 때까지 기다리고, .unwrap()으로 에러 무시하고 강제 추출합니다. .map(…) 부분은 4개의 스레드를 기다리며 각각의 계산된 합을 모아 [u64; 4] 형태로 만듭니다
  • .sum()은 [u64; 4]을 전부 더해서 최종 합계를 구해서, total에 할당합니다.
println!("합계: {}", total);
  • “합계: 500000500000″을 출력합니다.
    500000500000은 1000000 * 1000001 / 2 = 500000500000입니다.

나. Python (threading 사용, CPU 병렬 처리 실패 예)

import threading

data = list(range(1, 1_000_001))
results = [0] * 4

def worker(idx, chunk):
results[idx] = sum(chunk)

threads = []
chunk_size = len(data) // 4

for i in range(4):
t = threading.Thread(target=worker, args=(i, data[i*chunk_size:(i+1)*chunk_size]))
threads.append(t)
t.start()

for t in threads:
t.join()

print("합계:", sum(results))

🔸 설명:

  • Python은 GIL(Global Interpreter Lock) 때문에 진짜 병렬 아님
  • threading은 CPU 병렬 처리 불가 → 오히려 느림
  • multiprocessing을 쓰면 병렬 가능하지만 복잡도 증가

다. Java 버전

import java.util.concurrent.*;

public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newFixedThreadPool(4);
int[] data = new int[1_000_000];
for (int i = 0; i < data.length; i++) data[i] = i + 1;

Future<Long>[] results = new Future[4];
int chunkSize = data.length / 4;

for (int i = 0; i < 4; i++) {
final int start = i * chunkSize;
final int end = (i + 1) * chunkSize;
results[i] = executor.submit(() -> {
long sum = 0;
for (int j = start; j < end; j++) sum += data[j];
return sum;
});
}

long total = 0;
for (Future<Long> result : results) {
total += result.get();
}

executor.shutdown();
System.out.println("합계: " + total);
}
}

🔸 설명:

  • ExecutorService를 통해 병렬 처리
  • 비교적 안전하지만, Future.get()이 blocking이며 예외 처리 필요
  • GC가 있어 런타임 예측 어려움

라. C++ 버전 (C++11 이상)

#include <iostream>
#include <vector>
#include <thread>
#include <numeric>

int main() {
std::vector<uint64_t> data(1'000'000);
std::iota(data.begin(), data.end(), 1);

uint64_t partial_sums[4] = {};
std::vector<std::thread> threads;

size_t chunk_size = data.size() / 4;

for (int i = 0; i < 4; ++i) {
threads.emplace_back([i, chunk_size, &data, &partial_sums]() {
partial_sums[i] = std::accumulate(
data.begin() + i * chunk_size,
data.begin() + (i + 1) * chunk_size,
0ULL
);
});
}

for (auto& t : threads) t.join();

uint64_t total = 0;
for (auto s : partial_sums) total += s;

std::cout << "합계: " << total << std::endl;
}

🔸 설명:

  • 고성능이지만, &data, &partial_sums는 데이터 경쟁 가능성 존재
  • 공유 자원 동기화에 실패하면 잘못된 결과 나올 수 있음
  • 동기화 도구 사용 시 성능 저하 우려

마. 언어별 동시성 비교

언어병렬 처리 성능안전성코드 복잡도주의사항
Rust매우 뛰어남컴파일 타임 안전 보장다소 복잡소유권, 라이프타임 이해 필요
Python낮음 (GIL)안전하나 느림간단multiprocessing 사용 시 복잡
Java중간런타임 에러 가능보통예외 처리, GC
C++고성능데이터 레이스 가능복잡직접 동기화 필요

6. Rust vs Go 동시성 비교 요약

항목RustGo
동시성 모델명시적 스레드 + 채널 + async/await경량 고루틴(goroutine) + 채널(channel)
메모리 관리수동 + 소유권 시스템 (GC 없음)GC 있음 (자동 메모리 관리)
안전성컴파일 타임에 데이터 경쟁 차단런타임에 데이터 레이스 가능 (race detector 필요)
런타임없음 (zero-cost abstraction)있음 (스케줄러 + GC)
학습 곡선가파름 (소유권/라이프타임 개념 필요)비교적 완만
성능매우 뛰어남 (GC 없음)빠르지만 GC 오버헤드 존재

7. 10개의 작업을 동시 실행 비교

가. Rust:

use std::thread;

fn main() {
let mut handles = vec![];

for i in 0..10 {
let handle = thread::spawn(move || {
println!("Rust 스레드 {} 실행", i);
});
handles.push(handle);
}

for handle in handles {
handle.join().unwrap();
}
}

✅ 특징:

  • std::thread::spawn으로 OS 스레드 생성
  • 스레드 수 제한 없음 (하지만 무거움)
  • 안전하게 소유권 이동 (move) → 데이터 경쟁 없음

나. Go: 고루틴 10개 실행

package main

import (
"fmt"
"time"
)

func main() {
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Printf("Go 고루틴 %d 실행\n", i)
}(i)
}

time.Sleep(time.Second) // 고루틴이 끝날 시간 대기
}

✅ 특징:

  • go 키워드 하나로 병렬 실행
  • 고루틴은 스레드보다 훨씬 가볍고 수천 개 생성 가능
  • 단, 공유 자원 접근 시 데이터 레이스 가능 (예: i 변수 캡처 문제 발생 가능)

다. 동시성 핵심 차이

항목RustGo
실행 단위OS 스레드 또는 async task고루틴 (경량 스레드)
병렬 처리 수단스레드, 채널, async/await고루틴, 채널
데이터 보호컴파일 타임 소유권 체크뮤텍스, 채널, race detector
동시성 철학“Fearless Concurrency” (두려움 없는 동시성)“Do not communicate by sharing memory…”
GC없음 (직접 메모리 관리)있음 (자동 정리되지만 성능 오버헤드 발생 가능)

라. 성능과 안전성 비교

항목RustGo
성능GC가 없어서 시스템 자원 최대 활용GC와 스케줄러의 오버헤드 존재
안전성데이터 경쟁을 컴파일 타임에 방지기본적으로 가능함 (race detector로 검사해야 함)
스케일링수천 개의 작업 처리 시 async 필요수천 개 고루틴도 가볍게 처리 가능
디버깅 난이도복잡 (라이프타임, borrow checker 등)비교적 단순

마. 상황별 언어 선택

상황Rust 추천Go 추천
고성능 시스템 (e.g. 게임, 실시간 처리, WebAssembly)X
빠른 개발, 유지보수 쉬운 서버 (e.g. 웹 API, 클라우드 백엔드)가능하지만 무겁고 복잡매우 적합
메모리 제어 필요 (e.g. 임베디드, 드라이버)X
초고성능 네트워크 서버 (e.g. Actix, Tokio 기반)GC로 한계 가능
간단한 병렬 작업, CLI 툴한계

바. 결론

항목RustGo
성능최상급좋음 (하지만 GC 존재)
안전성컴파일 타임 보장런타임 race 가능
개발 속도어렵고 장벽 높음빠르고 쉬움
확장성 (스케일)async 사용 시 매우 뛰어남고루틴 덕분에 뛰어남
유지보수성복잡간단하고 명확

사. 요약

  • Rust: 동시성을 정밀하게 제어해야 하거나, 성능과 안전이 최우선인 경우 유리
  • Go: 빠르게 개발하고, 다수의 작업을 단순하게 병렬 처리할 때 탁월

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다