동시성(Concurrency)과 병렬성(Parallelism)
-
동시성: 동시에 여러 작업을 ‘처리’하는 개념입니다. 예를 들어, 요리사가 파스타를 삶고, 소스를 만들고, 양파를 써는 등 여러 작업을 번갈아 가며 진행하는 것과 같습니다.
-
병렬성: 동시에 여러 ‘행동’을 취하는 개념입니다. 이는 하드웨어(예: CPU 코어)의 지원을 받아 실제로 여러 작업을 동시에 실행하는 것을 의미합니다. 양파 써는 작업을 세 배로 늘리려면 세 명의 사람이 필요하듯, 컴퓨터에서는 추가 CPU 코어가 필요합니다.
루비의 동시성 프리미티브
루비는 동시성 구현을 위해 주로 세 가지 프리미티브를 제공합니다.
1. 프로세스 (Process)
-
특징: 운영체제가 제공하는 독립적인 실행 단위입니다. 각 프로세스는 고유한 메모리 공간, 파일 핸들, 프로세스 ID 등을 가집니다.
fork를 통해 새로운 프로세스를 생성할 수 있습니다. -
오버헤드: 프로세스를 생성하는 데는 많은 오버헤드가 발생합니다. 모든 자원을 복제하거나 공유해야 하기 때문입니다.
-
병렬성: 프로세스는 진정한 병렬 처리를 가능하게 합니다. 각 프로세스는 별도의 CPU 코어에서 독립적으로 실행될 수 있습니다.
-
최적 활용: CPU 코어 수에 맞춰 정적인 수의 프로세스를 생성하는 것이 효율적입니다. 웹 서버에서는 Unicorn이나 Pitchfork와 같은 pre-forking 방식이 이에 해당합니다. CPU 코어 수를 초과하는 프로세스는 성능 저하를 유발합니다.
2. 스레드 (Thread)
-
특징: 프로세스 내에서 실행되는 경량 실행 단위입니다. 부모 프로세스의 메모리 공간, 파일 핸들 등 많은 자원을 공유하므로 프로세스보다 생성 오버헤드가 적습니다.
-
GVL (Global VM Lock): C Ruby에서 스레드는 GVL의 제약을 받습니다. GVL은 한 번에 하나의 스레드만이 루비 코드를 실행할 수 있도록 허용합니다. 따라서 루비 코드(예: 피보나치 계산)는 스레드를 사용해도 병렬로 실행되지 않고 직렬로 처리됩니다.
-
IO 바운드 작업: GVL은 IO 작업(예:
sleep, 데이터베이스 호출) 중에는 해제될 수 있습니다. 이 경우 다른 스레드가 루비 코드를 실행하거나 다른 IO 작업을 수행할 수 있어, IO 바운드 작업에서는 스레드가 병렬성을 제공합니다. -
최적 활용: IO 바운드 작업이 많은 경우 스레드 풀을 사용하여 효율성을 높일 수 있습니다. Puma와 같은 웹 서버는 프로세스와 스레드를 결합하여 이 모델을 활용합니다.
3. 파이버 (Fiber / Co-routine)
-
특징: 스레드 내에서 실행되는 가장 경량화된 실행 단위입니다. 스레드보다 훨씬 저렴하며, 하나의 스레드 안에 수많은 파이버를 생성할 수 있습니다.
-
협력적(Cooperative): 파이버는 선점형(preemptive) 스레드와 달리, 스케줄러가 강제로 제어를 빼앗지 않습니다. 파이버는 명시적으로
yield를 호출하거나 IO 작업이 발생할 때 자발적으로 제어권을 넘겨야 합니다. -
스케줄러: 루비에는 내장된 파이버 스케줄러가 없으므로,
async젬과 같은 외부 라이브러리를 사용해야 합니다. -
한계: CPU 바운드 작업에서 한 파이버가 장시간 실행되면 다른 파이버의 실행이 지연될 수 있습니다. IO 바운드 작업에 매우 효율적입니다.
-
최적 활용: Falcon과 같은 파이버 기반 서버는 이러한 특성을 활용하여 높은 동시성을 달성합니다. 특히 IO 작업이 많은 환경에서 뛰어난 성능을 보입니다.
초기 문제의 해결
발표 초기에 언급된 ‘PDF 생성 큐가 스레드 수를 늘려도 느려지는’ 문제는 CPU 바운드 작업(PDF 생성)에 과도하게 많은 스레드를 사용했기 때문입니다. GVL로 인해 20개의 스레드가 있더라도 한 번에 하나의 스레드만 루비 코드를 실행할 수 있었고, 나머지 스레드는 대기 상태에 있었습니다. 또한 100밀리초마다 발생하는 불필요한 컨텍스트 전환 비용이 누적되어 오히려 성능이 저하되었습니다. 해결책은 CPU 코어 수에 맞춰 프로세스 수를 늘려 진정한 병렬 처리를 가능하게 하는 것이었습니다.