루비는 다양한 메커니즘을 통해 동시성과 병렬성을 지원합니다. 첫 번째는 스레드입니다. 프로세스에 비해 경량이며 메모리를 공유한다는 장점이 있습니다. 그러나 MRI(Matz’s Ruby Interpreter) 루비에서는 GIL(Global Interpreter Lock), 즉 전역 인터프리터 잠금으로 인해 진정한 병렬성(Parallelism)을 달성하기 어렵습니다. GIL은 한 번에 하나의 스레드만 실행되도록 허용하므로, 여러 스레드가 동시에 실행되는 것처럼 보이지만 실제로는 빠르게 컨텍스트 스위칭(Context Switching)하며 번갈아 실행되는 동시성(Concurrency)에 가깝습니다. IO(입출력) 집약적인 작업(예: 웹 스크래핑, API 호출)에서는 스레드를 통해 상당한 성능 향상(예: 2.5초에서 0.14초로 단축)을 얻을 수 있습니다. 하지만 스레드 사용 시에는 공유 상태로 인한 경쟁 조건(Race Condition)이나 교착 상태(Deadlock)와 같은 문제에 직면할 수 있으며, 이를 해결하기 위해 뮤텍스(Mutex) 잠금, 원자적(Atomic) 연산, 잠금 순서 정렬과 같은 동기화 기법이 필수적입니다. 또한, 운영체제 수준에서 발생하는 컨텍스트 스위칭 비용이 비교적 높다는 단점이 있습니다.
두 번째는 파이버입니다. 파이버는 코루틴(Coroutines)과 유사한 경량 스레드로 간주될 수 있습니다. 스레드와 달리 파이버의 컨텍스트 스위칭은 개발자(Fiber.yield
, Fiber.resume
)에 의해 명시적으로 제어됩니다. 이는 운영체제의 개입 없이 이루어지므로 스레드에 비해 컨텍스트 스위칭 비용이 훨씬 저렴하며, 수백, 수천 개의 파이버를 생성하는 것이 스레드를 생성하는 것보다 훨씬 효율적입니다. 파이버 역시 IO 집약적인 작업에서 뛰어난 성능 향상(예: 6초에서 3.8초로 단축)을 보여주며 동시성을 달성하는 데 효과적입니다. 루비 3.0부터는 스케줄러(Scheduler)를 통해 IO 대기 시 다른 파이버로 전환하는 등의 최적화가 가능해졌습니다. async
gem과 같은 비동기 라이브러리를 활용하면 파이버를 보다 쉽게 관리할 수 있습니다. 하지만 파이버는 스레드와 마찬가지로 진정한 병렬성을 제공하지 않으며, 특정 파이버가 제어권을 양보하지 않으면 다른 파이버가 실행되지 못하는 스타베이션(Starvation) 문제가 발생할 수 있습니다.
세 번째는 리액터입니다. 리액터는 루비에서 진정한 병렬성을 달성하기 위한 옵션입니다. 이는 경량 프로세스와 유사하게 동작하며, 각 리액터는 독립적인 메모리 공간을 가집니다. 이 메모리 격리 덕분에 스레드에서 흔히 발생하는 경쟁 조건 문제를 원천적으로 방지할 수 있습니다. 리액터 간의 통신은 send
, receive
, yield
, take
와 같은 메시지 전달 메커니즘을 통해 이루어지며, 이때 전달되는 데이터는 반드시 불변(Immutable)해야 합니다. 리액터는 GIL의 영향을 받지 않으므로, 다수의 리액터가 여러 CPU 코어에서 동시에 실행되어 CPU 리소스를 효율적으로 활용할 수 있습니다. 이는 특히 CPU 집약적인 작업(예: 대용량 CSV 파일 처리 및 계산)에서 상당한 성능 향상(예: 4.7초에서 1.6초로 단축)을 가져옵니다. 현재 리액터는 실험적인 기능으로, 일부 기존 라이브러리와의 호환성 문제가 있을 수 있습니다.