-
스레드 (Threads): 스레드는 단일 프로세스 내에서 여러 실행 흐름을 생성하여 동시성을 달성하는 방법입니다. 프로세스에 비해 생성 비용이 저렴하고 메모리를 공유한다는 장점이 있습니다. Ruby의 MRI(Matz’s Ruby Interpreter) 환경에서는 GIL(Global Interpreter Lock)로 인해 진정한 병렬성(Parallelism)을 달성하기 어렵습니다. GIL은 한 번에 하나의 스레드만 실행되도록 허용하므로, 여러 스레드가 동시에 CPU를 사용하는 것이 불가능합니다. 그러나 API 호출이나 웹 스크래핑과 같은 I/O 바운드(IO-bound) 작업에서는 스레드가 대기하는 동안 다른 스레드가 실행될 수 있어 효율성을 높일 수 있습니다. 하지만 스레드 사용 시에는 공유 자원에 대한 접근 제어(뮤텍스 락, 동기화)가 필수적이며, 그렇지 않으면 경쟁 조건(Race Condition)이나 교착 상태(Deadlock)와 같은 문제가 발생할 수 있습니다. 발표자는 은행 계좌 이체 시뮬레이션을 통해 경쟁 조건으로 인한 데이터 불일치 문제를 시연하고, 이를 해결하기 위한 동기화 및 락킹(Locking)의 중요성을 강조합니다. 또한, 운영체제 수준에서 발생하는 컨텍스트 스위칭(Context Switching) 비용으로 인해 스레드가 많아질수록 성능 저하가 발생할 수 있습니다.
Concurrent Ruby
와 같은 라이브러리는 이러한 문제들을 효과적으로 관리하기 위한 데이터 구조와 메서드를 제공합니다. -
파이버 (Fibers): 파이버는 코루틴(Coroutines)과 유사한 개념으로, 스레드보다 훨씬 경량화된 동시성 도구입니다. 파이버의 가장 큰 특징은 컨텍스트 스위칭이 개발자에 의해 명시적으로 제어된다는 점입니다. 즉,
Fiber.yield
를 통해 현재 파이버의 실행을 일시 중지하고 다른 파이버로 제어를 넘길 수 있습니다. 이는 운영체제가 아닌 애플리케이션 수준에서 컨텍스트 스위칭이 이루어지므로 스레드에 비해 비용이 훨씬 저렴합니다. 따라서 수백, 수천 개의 동시 작업을 처리해야 할 때 스레드보다 효율적입니다. 파이버는 주로 I/O 바운드 작업에 적합하며 동시성을 제공하지만, 스레드와 마찬가지로 진정한 병렬성은 제공하지 않습니다. 또한, 개발자가 직접 컨텍스트 스위칭을 관리해야 하므로 코드 복잡성이 증가할 수 있으며, 특정 파이버가 제어를 양보하지 않을 경우 다른 파이버가 실행되지 못하는 스타베이션(Starvation) 문제가 발생할 수 있습니다.async
젬과 같은 비동기 라이브러리는 이러한 파이버 기반의 비동기 작업을 추상화하여 개발자가 더 쉽게 동시성 코드를 작성할 수 있도록 돕습니다. Ruby 3.0부터는 스케줄러(Scheduler)를 통해 I/O 대기 시 자동으로 파이버를 전환하는 훅(hook)을 제공하여 개발 편의성을 높였습니다. -
랙터 (Ractors): 랙터는 Ruby 3.0에 도입된 기능으로, 진정한 병렬성을 달성하기 위한 도구입니다. 랙터는 독립된 메모리 공간을 가지는 경량 프로세스와 유사하게 동작합니다. 이는 스레드와 달리 메모리를 공유하지 않으므로, 경쟁 조건과 같은 공유 상태 문제에서 자유롭습니다. 랙터 간의 통신은 메시지 전달(send/receive) 방식을 통해 이루어지며, 이때 전달되는 데이터는 불변(immutable)해야 합니다. 랙터는 CPU 바운드(CPU-bound) 작업, 예를 들어 대용량 CSV 파일 처리나 복잡한 계산 작업에 특히 효과적입니다. GIL의 제약을 받지 않고 여러 랙터가 동시에 다른 CPU 코어에서 실행될 수 있기 때문입니다. 발표자는 대용량 CSV 파일 처리 예시를 통해 랙터가 순차 처리 대비 2~3배의 성능 향상을 가져올 수 있음을 보여줍니다. 하지만 랙터는 아직 실험적인 기능이므로, 기존의 많은 젬(Gem)들이 랙터와 호환되지 않을 수 있으며, 불변 데이터 공유 방식에 대한 이해가 필요하다는 점이 단점으로 지적됩니다.
Ruby에서의 동시성(Concurrency)과 병렬성(Parallelism) - 스레드, 파이버, 랙터
Magesh, "Concurrency in Ruby: Threads, Fibers, and Ractors Demystified"
작성자
EuRuKo
발행일
2025년 02월 24일
핵심 요약
- 1 Ruby는 CPU 활용 효율을 높이기 위해 스레드, 파이버, 랙터와 같은 동시성 및 병렬성 도구를 제공합니다.
- 2 스레드와 파이버는 주로 I/O 바운드 작업에 적합하며 동시성을 제공하지만, 랙터는 CPU 바운드 작업에 적합하며 진정한 병렬성을 구현합니다.
- 3 각 도구는 고유한 장단점과 사용 사례를 가지므로, 작업 특성에 맞춰 적절한 도구를 선택하는 것이 중요합니다.
도입
현대 컴퓨터는 다중 코어 CPU를 탑재하고 있음에도 불구하고, 많은 애플리케이션이 단일 코어만 효율적으로 사용하여 전체 CPU 성능의 일부만을 활용하는 경우가 많습니다. 이는 CPU의 발전 속도에 비해 소프트웨어의 동시성 및 병렬성 활용이 뒤처지고 있기 때문입니다. 효율적인 자원 활용과 성능 향상을 위해서는 동시성 프로그래밍이 필수적입니다. 본 발표는 Ruby 언어에서 제공하는 스레드(Threads), 파이버(Fibers), 랙터(Ractors) 세 가지 주요 동시성 및 병렬성 도구를 소개하고, 각 도구의 특징, 장단점, 그리고 적절한 사용 사례를 탐구합니다.
결론
Ruby는 다양한 동시성 및 병렬성 요구 사항을 충족시키기 위해 스레드, 파이버, 랙터라는 세 가지 강력한 도구를 제공합니다. 스레드는 소수의 I/O 바운드 동시 작업에 적합하며, 파이버는 스레드보다 더 많은 I/O 바운드 동시 작업을 효율적으로 처리할 수 있습니다. 반면, 랙터는 CPU 집약적인 작업에 진정한 병렬성을 제공하여 다중 코어 CPU의 잠재력을 최대한 활용할 수 있게 합니다. 각 도구의 특성과 장단점을 이해하고, 애플리케이션의 특정 요구사항(I/O 바운드 또는 CPU 바운드, 동시성 또는 병렬성)에 따라 적절한 도구를 선택하는 것이 Ruby 애플리케이션의 성능과 효율성을 극대화하는 핵심입니다. 이러한 도구들을 통해 개발자는 더욱 반응성 높고 확장 가능한 시스템을 구축할 수 있습니다.