Textbook review: Is Parallel Programming Hard, And, If So, What Can You Do About It?
교과서 리뷰: 병렬 프로그래밍은 어려운가요? 그렇다면 어떻게 대응해야 하나요?
Linux 커널 RCU를 만든 Paul E. McKenney의 무료 교재를 읽은 뒤, CPU 캐시와 컴파일러 최적화부터 메모리 모델과 락프리 프로그래밍까지 평가합니다. 리뷰어는 초반 다섯 장만 깊이 읽었지만 병렬 프로그래밍을 더 공부하고 싶게 만든 훌륭한 입문서라고 봅니다.
- 주제
AI 요약
이 글은 Paul E. McKenney가 쓴 무료 온라인 교재 《Is Parallel Programming Hard, And, If So, What Can You Do About It?》를 읽고 정리한 리뷰입니다. McKenney는 Linux 커널의 RCU(Read-Copy-Update) 동기화 메커니즘을 만든 인물입니다. 리뷰어는 지난 10년 동안 TLA⁺와 분산 시스템을 주로 다뤘지만, 멀티코어 CPU에서 실제로 동시에 발생하는 이벤트와 락프리(lock-free) 알고리즘을 충분히 이해하지 못했다는 사실을 깨닫고 이 책을 골랐습니다.
읽게 된 계기와 교재 구성
리뷰어는 2026년 Software Should Work 콘퍼런스에서 Fil-C를 발표한 Filip Pizlo와 대화하면서 동시성에 관한 관점을 바꿨다고 설명합니다. 특히 동시 락프리 가비지 컬렉터를 작성하는 일이 얼마나 어려운지 들으면서, TLA⁺와 분산 시스템 지식만으로는 멀티코어 환경의 동시성을 충분히 다루기 어렵다고 느꼈습니다. 실제 동시성 사건을 추론할 때 TLA⁺가 편리하지 않을 수 있다는 점, 동시 알고리즘을 선형화 가능성(linearizability) 관점에서 분석해야 한다는 점도 관심을 끌었습니다.
교재는 PDF 세 가지 형식으로 제공됩니다. 과학 논문처럼 두 단으로 배치한 형식, 여백이 넓은 한 단 형식, 여백을 없앤 한 단 형식입니다. 마지막 형식은 Pine64 PineNote에서 읽기 좋았다고 합니다. 본문에는 지식 확인 문제의 정답, 그림과 절의 위치, 각주와 인용으로 이어지는 내부 링크가 매우 많습니다. 지식 확인 문제는 유용하지만, 나머지 링크는 전자책에서 페이지를 넘길 때 실수로 누르기 쉽습니다. 두 링크가 나란히 놓인 곳에서는 터치스크린으로 원하는 링크만 고르기도 어렵습니다. 링크를 모두 끄면 지식 확인 문제까지 잃게 되므로, 리뷰어는 불편을 감수하고 읽었습니다.
하드웨어와 컴파일러가 만드는 동시성의 함정
3장 ‘Hardware and its Habits’는 현대 CPU가 무엇 때문에 빠르고 무엇 때문에 느린지 설명합니다. 리뷰어가 특히 흥미롭게 본 부분은 캐시에 없는 메모리 주소에 CPU 코어가 쓰기 작업을 수행하는 과정을 단순화해 보여준 3.2.1절입니다. 다만 MESI 프로토콜을 기본 수준에서 설명하지 않은 점은 아쉬웠다고 합니다. 리뷰어는 별도로 MESI를 찾아본 뒤 나머지 내용을 훨씬 잘 이해하게 됐습니다.
MESI를 공부하면서 x86 CPU의 여러 코어가 같은 데이터 위치에 글자 그대로 동시에 쓰는 방식은 아니라는 점도 배웠습니다. x86 CPU 코어는 메모리에 직접 쓰기보다 캐시에 기록하고, 해당 캐시라인(cache line)의 배타적 소유권을 얻은 뒤 값을 변경합니다. 다른 코어가 같은 주소에 쓰려면 소유권이 넘어오기를 기다려야 합니다. 다만 기록 대상이 여러 캐시라인에 걸치면 쓰기 찢김(store tearing)이 발생할 수 있습니다.
4장 ‘Tools of the Trade’에서는 병렬 프로그램을 컴파일하고 실행할 때 발생하는 더 난해한 문제를 다룹니다. 컴파일러는 공유 변수에 관한 코드에서 로드 찢김(load tearing), 저장 찢김(store tearing), 로드 병합(load fusing), 저장 병합(store fusing), 코드 재배치, 컴파일러가 만들어내는 로드와 저장, 저장-로드 변환, 데드 코드 제거 같은 변환을 수행할 수 있습니다. 컴파일러 단계를 통과한 뒤에도 CPU가 실행 과정에서 예상 밖의 동작을 보일 수 있습니다. 리뷰어는 익숙한 뮤텍스나 메시지 전달 방식에서 벗어나 병렬 프로그램을 생각하기 어려울 정도로 이 장이 혼란스러웠다고 말합니다.
아쉬운 점은 이 장의 설명이 Linux 커널 문맥에 지나치게 치우쳤다는 부분입니다. C11과 C++11이 std::memory_order 같은 기능으로 병렬 프로그래밍을 어떻게 형식화했는지 더 자세히 다루기를 기대했지만, 관련 내용은 C11 원자 연산과 최신 GCC를 설명하는 짧은 절에 머뭅니다. ACCESS_ONCE()와 WRITE_ONCE() 매크로가 사실상 volatile* 캐스팅을 활용해 컴파일러의 변환을 막는다는 정도의 막연한 이해만 남았다고 합니다. 선의의 데이터 경쟁(benign data race)을 오류로 볼지 논쟁했던 역사도 빠져 있습니다.
카운터 구현으로 배우는 성능과 거짓 공유
5장 ‘Counting’은 책의 대표적인 장으로 평가합니다. 여러 스레드가 하나의 카운터를 증가시키는 프로그램을 약 10가지 방식으로 구현하고 비교합니다. 각 스레드가 원자적 증가 명령을 사용하는 단순하고 올바른 구현은 초반에 소개하지만, 성능이 매우 나쁘다는 점을 곧바로 보여줍니다. 여러 코어가 같은 캐시라인의 소유권을 차지하려고 경쟁하기 때문입니다.
장 후반에는 ‘signal-theft limit counter’라는 기법까지 나옵니다. 리뷰어는 이 기법을 완전히 이해하지 못했고, 직접 카운터를 작성한다면 그 정도까지 복잡하게 만들지는 않을 것 같다고 말합니다. 대신 스레드별 통계 카운터를 배열로 두는 방식은 좋게 평가합니다. 분산 시스템에서 사용하는 CRDT(Conflict-free Replicated Data Type)와 닮았고, 거짓 공유(false sharing)의 성능 영향을 설명하는 사례로도 적절합니다. 스레드별 카운터를 하나의 연속된 배열에 단순히 배치하면 같은 캐시라인을 여러 스레드가 공유하게 되므로, 스레드별 데이터를 분리하는 설계가 필요합니다.
후반부 주제와 전체 평가
5장 이후에는 관심 있는 부분을 중심으로 빠르게 읽었습니다. 소유권, 파티셔닝, 지연 처리처럼 분산 시스템 지식과 연결되는 개념도 나옵니다. 형식 검증 장에서는 Promela와 Spin을 사용하지만, TLA⁺ 사용자인 리뷰어는 새 도구를 익힐 동기가 크지 않았습니다. 검증 장의 11.6.4절 ‘Hunting Heisenbugs’는 좋은 내용으로 꼽았습니다.
락프리 프로그래밍은 14장 ‘Advanced Synchronization’에 이르러서야 본격적으로 등장합니다. 리뷰어는 그 시점에 락프리 프로그래밍과 자료구조에 특화된 다른 교재를 찾고 싶어졌습니다. 15장에서는 메모리 순서를 설명하지만, 이미 다른 자료를 찾아 혼란을 해소하려던 상태였다고 합니다.
전체적으로는 초반 다섯 장을 깊이 읽은 정도지만, 교재를 훌륭하게 평가합니다. 책이 병렬 프로그래밍을 더 공부하고 싶게 만들었기 때문입니다. 리뷰어는 릴리스-컨슘(release-consume) 순서 형식화의 실패, x86에서 정렬된 mov 로드와 저장의 원자성, out-of-thin-air 값, DEC Alpha의 약한 메모리 모델, 릴리스-어콰이어(release-acquire) 의미론, 락프리 알고리즘을 결정적 시뮬레이션 테스트로 아직 제대로 검증하기 어려운 문제, pre-v8 ARM에서 원자 연산이 선점될 수 있다는 사실을 더 알아가며 동료들에게 배운 내용을 말하고 싶어졌다고 전합니다. 다음 관심사는 고성능 가비지 컬렉션이며, 관련 추천을 요청하며 글을 마무리합니다.
원문: Ahelwer.ca / 번역·요약: Trawling