Racket v9.0의 병렬 스레드는 개발자가 멀티코어 시스템의 이점을 쉽게 활용할 수 있도록 설계되었습니다. 이는 Racket의 오랜 병렬 처리 여정의 정점입니다.
병렬 스레드 도입
병렬 스레드를 생성하는 것은 thread 호출에 #:pool 'own 플래그를 추가하는 것만큼 간단합니다. 이는 새로운 스레드가 자체 병렬 스레드 풀에서 실행되도록 합니다. 또한, #:keep 'result 인수를 추가하면 스레드 실행 완료 후 결과를 유지할 수 있으며, thread-wait 함수로 결과를 검색할 수 있습니다. 기본적으로 thread 함수는 여전히 코루틴 스레드를 생성하여 하위 호환성을 유지합니다. 코루틴 스레드는 경량이며 선점형으로 스케줄링되어 GUI 상호작용이나 원격 프로세스 오케스트레이션과 같은 동시성 이점이 필요한 작업에 여전히 유용합니다.
Racket의 병렬 처리 여정
Racket은 1990년대 중반에 시작되었으며, 당시에는 멀티프로세서 컴퓨터가 흔치 않아 단일 스레드 런타임의 단순성을 활용했습니다. 이후 places와 futures를 통해 병렬 처리를 지원했지만, 일반적인 병렬 작업에 적용하기에는 특별한 노력이 필요했습니다. 2017년부터 Chez Scheme을 기반으로 Racket을 재구축(Racket CS)하면서 병렬 처리를 위한 더 나은 기반이 마련되었습니다. Chez Scheme의 메모리 일관성 모델 개선과 병렬 가비지 컬렉터 도입은 Racket CS에서 병렬화의 가능성을 크게 높였습니다.
성능 분석
M2 Mac에서의 벤치마크 결과는 병렬 스레드의 성능 이점과 현재 한계를 보여줍니다.
벤치마크 결과 요약
-
단순 수치 연산 (Fibonacci): 4개 병렬 스레드에서 3.7배, 8개 스레드에서 5.4배의 뛰어난 속도 향상을 보였습니다.
futures와 유사한 성능을 나타냈습니다. -
문자열 변환 및 할당 (strfib*): 문자열 변환 및 빈번한 할당이 있는 작업에서도 4개 스레드에서 3.7배, 8개 스레드에서 3.9배의 속도 향상을 보였으며, 병렬 가비지 컬렉션의 효과를 확인할 수 있었습니다.
-
파라미터 의존 작업 (strfib):
string->number와 같이 파라미터 값에 의존하는 작업에서futures는 블록되는 반면, 병렬 스레드는 4개 스레드에서 4배, 8개 스레드에서 4.3배의 속도 향상을 제공하여 더 일관된 이점을 제공했습니다. -
해시 테이블 작업 (hash-nums):
equal?-기반 해시 테이블 작업에서도futures는 블록되지만, 병렬 스레드는 4개 스레드에서 3.7배, 8개 스레드에서 4.5배의 성능 향상을 달성했습니다.
병렬 스레드의 한계
-
I/O 작업 (hash-digs, hash-dir): 바이트 스트링 포트에 데이터를 쓰고 해시하는 작업이나 디렉토리 내 파일들을 해시하는 작업에서는 병렬 스레드가 일부 속도 향상을 보였지만, I/O 계층의 굵은 잠금(coarse-grained locks)으로 인해 확장성이 크게 제한되었습니다.
-
순차 프로그램에 대한 오버헤드: 병렬 스레드 지원을 위한 잠금 메커니즘은 병렬 스레드를 사용하지 않는 순차 프로그램에도 최대 6-8%의 약간의 성능 저하를 초래할 수 있습니다.
하위 호환성 및 고려사항
Racket의 기존 동기화 구조(세마포어, 채널 등)는 병렬 스레드에서도 동일하게 작동하며, box-cas!와 같은 비교-교환(compare-and-swap) 연산도 프로세서 수준의 기본 요소를 사용하므로 안전합니다.
메모리 일관성 모델
병렬 스레드는 기본 머신의 약한 메모리 일관성 모델을 노출할 수 있지만, Racket(정확히는 Chez Scheme)은 메모리 펜스를 사용하여 메모리 안전성을 항상 보장합니다. 이는 기존의 세대별 가비지 컬렉션 및 futures의 메모리 안전성을 지원하는 것과 동일한 메커니즘입니다.
아토믹 모드
병렬 스레드가 아토믹 모드에 진입하면 다른 코루틴 스레드의 실행은 방지하지만, 다른 병렬 스레드의 실행은 방지하지 않습니다. 병렬 스레드에서 아토믹 모드 진입은 코루틴 스레드보다 훨씬 비용이 많이 들 수 있습니다.
외래 함수 호출
코루틴 스레드에서 외래 함수 호출은 사실상 아토믹하지만, 병렬 스레드에서는 그렇지 않습니다. 그러나 대부분의 외래 함수 바인딩은 이미 운영체제 스레드와 호환되도록 설계되었습니다.