2.17

View in English

2.17 동시성과 병렬성

개요와 동기

동시성은 프로그램을 서로를 기다리지 않고 진행할 수 있는 독립적인 작업으로 구조화하는 기술입니다. 병렬성은 그 작업을 여러 프로세서에서 실제로 같은 순간에 실행하는 것입니다. 이 구분은 현학적이지 않습니다. 동시성은 느린 네트워크 호출이 프로그램 전체를 멈추지 않게 코드를 조직하는 방법이고, 병렬성은 큰 계산을 코어에 퍼뜨려 더 빨리 끝내는 방법입니다. 둘을 혼동하면 팀이 속도를 바라고 스레드를 추가했다가 버그만 얻습니다.

큰 팀에서 이 주제가 중요한 이유는 동시성이 정확성이 조용히 죽는 곳이기 때문입니다. 단일 스레드 코드를 쓰는 한 작성자는 한 줄씩 추론할 수 있지만, 많은 작성자가 스레드 사이에 메모리를 공유하는 순간 가능한 인터리빙의 수가 폭발하고, 모든 테스트를 통과하는 프로그램도 프로덕션 부하에서 백만 번에 한 번 실패할 수 있습니다. 요란한 크래시가 아니라 손상된 데이터, 멈춘 요청, 아무도 재현할 수 없는 인시던트로 나타납니다. 이 장은 2.16장(성능 엔지니어링)의 코드 수준 초점과 2.13장의 컴퓨팅 기초 위에 서서, 기계를 가로지르는 동시성인 3.3장(분산 시스템)의 조율 문제로 이어집니다. 후자는 신뢰할 수 없는 네트워크라는 잔혹함이 더해집니다.

기업에서 동시성 버그는 처리량 버그입니다. 트래픽이 높은 서비스는 공유 상태를 두고 경쟁하지 않고 수천 개의 동시 요청을 처리하는 능력에 달려 있으며, 동기화되지 않은 카운터 하나가 부하 아래서 원장을 손상시킬 수 있습니다. 정부에서 이해관계는 정확성과 감사 가능성입니다. 수십 년 운영되고 안전, 급여, 공공 기록을 건드리는 시스템입니다. 세무나 보건 시스템의 경쟁 상태는 불편이 아니라 누군가 나중에 감독 기관에 설명해야 하는 잘못된 답입니다. 두 환경 모두 목표는 같습니다. 안전한 경로를 기본으로 만들어, 코드를 만지는 많은 사람이 각자 동시성 전문가일 필요가 없게 하는 것입니다.

핵심 원칙

  • 동시성은 구조이고 병렬성은 실행입니다. 스레드에 손을 뻗기 전에 실제로 필요한 것이 어느 쪽인지 정하십시오.
  • 공유된 가변 상태가 적입니다. 거의 모든 동시성 버그는 두 작업이 같은 변하는 데이터를 건드리는 데로 거슬러 올라갑니다.
  • 불변성과 메시지 전달을 선호하십시오. 바뀔 수 없는 데이터는 경쟁할 수 없고, 메시지가 공유 메모리보다 안전합니다.
  • 비결정성이 핵심 난점입니다. 천 번에 한 번 나타나는 버그가 엣지 케이스가 아니라 문제 전체입니다.
  • 모든 것에 한계를 두십시오. 한계 없는 큐, 스레드 수, 진행 중인 작업은 급증을 장애로 바꿉니다.
  • 더 높은 수준의 모델이 날 것의 잠금을 이깁니다. 액터, 채널, 구조적 동시성은 많은 작성자에게 안전한 기본값을 주고, 모든 잠금에는 비용이 있습니다.
  • 행복한 경로만이 아니라 인터리빙을 테스트하십시오. 결정적 테스트는 드문 순서만이 드러내는 버그를 잡을 수 없습니다.

권장 사항

동시성이 필요한지 병렬성이 필요한지 정한다

문제에 이름을 붙이는 것으로 시작하십시오. 서비스가 대부분의 시간을 기다리는 데(데이터베이스, 네트워크 호출, 디스크) 쓴다면 I/O 바운드 워크로드이고, 동시성이 답입니다. 한 요청이 기다리는 동안 다른 요청이 진행되도록 코드를 구조화하십시오. async/await을 쓰는 단일 스레드나 작은 풀이 기다리는 수천 개의 요청을 처리할 수 있습니다. 반대로 프로그램이 CPU 바운드, 즉 기다림이 거의 없이 계산을 갈아대는 것이라면 코어에 걸친 병렬성이 속도를 사며, 여기서 상한은 암달의 법칙(2.16장 참조)이 정합니다. 직렬 비율이 코어를 아무리 더해도 속도 향상에 상한을 둡니다. 설계하기 전에 어느 체제에 있는지 측정하십시오.

공유된 가변 상태를 적으로 다룬다

거의 모든 동시성 결함은 같은 모양으로 환원됩니다. 두 작업이 순서에 합의하지 않고 같은 변하는 데이터를 읽고 씁니다. 이것이 경쟁 상태이며, 유실된 갱신, 반쯤 쓰인 객체, 코드가 안전하다고 가정한 불변식을 위반하는 값을 낳습니다. 가장 믿을 만한 방어는 공유된 가변 상태를 줄이는 것입니다. 각 작업에 자기 데이터를 주고, 참조가 아닌 복사본을 넘기고, 가변 상태를 다른 것이 메시지로 닿는 단일 소유자에 가두십시오. 정말 공유해야 한다면 공유를 명시적이고 작게 만들어, 리뷰어가 상태가 건드려지는 모든 곳을 볼 수 있게 하십시오.

불변성과 메시지 전달을 기본값으로 선호한다

가장 안전한 공유 데이터는 바뀔 수 없는 데이터입니다. 일단 만들어지면 불변 객체는 경쟁할 것이 없으므로 동기화 없이 얼마든지 많은 스레드가 읽을 수 있습니다. 불변성을 기본으로, 가변성을 의도적 예외로 삼으십시오. 작업이 조율해야 할 때는 공유 메모리보다 메시지 전달을 선호하십시오. 공통 변수를 공유하는 대신, 한 작업이 값을 다른 작업에 보냅니다. “메모리를 공유해서 소통하지 말고, 소통해서 메모리를 공유하라”는 Go 격언 뒤의 철학입니다. 메시지 전달은 보이지 않고 순서에 의존하는 버그를 명시적이고 검사 가능한 데이터 흐름으로 바꾸며, 그 명료함은 많은 사람이 유지하는 코드에서 메시지당 비용을 거의 항상 감수할 가치가 있습니다.

날 것의 잠금보다 먼저 높은 수준의 모델에 손을 뻗는다

손으로 쓴 잠금은 원리상 올바르고 실제로는 재앙입니다. 사람은 모든 인터리빙에 대해 추론하는 데 서툴기 때문입니다. 안전한 동시성을 기본으로 만드는 모델을 선호하십시오. 액터 모델은 각 액터에 비공개 상태와 메일박스를 줍니다. 액터는 메모리를 공유하지 않고 메시지만 보내므로 경쟁의 부류 전체가 사라집니다. Go의 채널 뒤의 모델인 통신 순차 프로세스(CSP)는 독립적인 프로세스가 타입이 있는 채널로 값을 전달합니다. 구조적 동시성은 동시 작업의 수명을 어휘적 범위에 묶어, 작업이 그것을 낳은 블록보다 오래 살 수 없고 오류가 사라지지 않고 전파되게 합니다. async/await은 I/O 바운드 동시성 코드를 순차적 스타일로 쓰게 합니다. 이 각각이 평균적인 작성자의 바닥을 끌어올리며, 이것이 큰 팀이 필요로 하는 것입니다.

메모리 모델, 원자성, 가시성을 이해한다

메모리를 공유할 때는 두 속성이 물립니다. 원자성은 연산이 한꺼번에 일어나거나 전혀 일어나지 않는다는 뜻입니다. 단순한 증가(x = x + 1)는 원자적이지 않습니다. 읽고, 더하고, 쓰는 세 단계를 다른 스레드가 끼어들 수 있어서 카운터가 갱신을 잃는 방식입니다. 가시성은 한 스레드의 쓰기가 다른 스레드에 관찰 가능해진다는 뜻입니다. 적절한 동기화가 없으면 한 코어에 쓰인 값이 다른 코어가 못 보는 캐시에 앉아 있을 수 있어서, 스레드가 이미 설정된 플래그를 두고 영원히 돌 수 있습니다. 언어의 메모리 모델은 쓰기가 언제 보이게 되는지와 컴파일러와 CPU가 어떤 순서를 재배열할 수 있는지 정의하므로, 코드가 쓴 순서대로 실행된다고 가정할 수 없습니다. 자체 락프리 방식을 발명하지 말고 언어의 원자 타입과 동기화 기본 요소를 쓰십시오.

동기화 기본 요소를 의도적으로 쓰고 교착 상태에 대비해 설계한다

공유가 불가피할 때는 올바른 기본 요소에 손을 뻗고 그 값을 존중하십시오. 락이나 뮤텍스(상호 배제)는 한 번에 한 스레드만 임계 구역에 들어가게 하지만 접근을 직렬화하므로, 뜨거운 락은 많은 코어의 이점을 지우는 병목이 됩니다. 세마포어는 한 번에 몇 개의 작업이 진행될 수 있는지 제한하며, 풀에 한계를 두는 방법입니다. 원자 연산은 카운터 같은 단순한 값에 락프리 갱신을 제공하며 락보다 싸지만 복합적인 것에는 잘못 쓰기 쉽습니다. 락은 세 가지 고전적 실패 양상을 가져옵니다. 교착 상태는 작업이 순환으로 서로를 기다려 아무도 진행하지 못하는 것으로, 두 스레드가 각각 락 하나를 쥐고 다른 하나를 원하는 것이 교과서적 사례입니다. 라이브락은 작업이 서로에게 계속 반응하지만 진전이 없는 것입니다. 기아는 다른 것들이 계속 새치기해서 작업이 자원을 얻지 못하는 것입니다. 이를 막는 규율은 구체적입니다. 전역 락 순서를 부과하고, 락을 짧게 쥐고, 막힌 작업이 요란하게 실패하도록 타임아웃을 더하고, 락을 쥔 채 알 수 없는 코드를 호출하지 말고, 기아가 위험한 곳에는 공정한 스케줄링을 쓰십시오. 이 규칙을 적어 두십시오. 새 작성자는 코드만으로는 이를 재발견할 수 없기 때문입니다.

백프레셔로 큐, 풀, 진행 중인 작업에 한계를 둔다

한계 없는 큐는 시한폭탄입니다. 트래픽 급증 시 작업이 빠지는 것보다 빨리 도착해 큐가 한없이 커지고, 메모리가 차고, 서비스는 과부하가 아니라 수수께끼 같은 메모리 부족 크래시처럼 보이는 방식으로 죽습니다. 모든 큐에 한계를 두고, 모든 스레드 풀에 상한을 두고, 백프레셔를 적용하십시오. 시스템이 가득 차면 끝낼 수 없는 무한한 작업을 받는 대신 상류에 느려지라고 신호하거나 작업을 빠르게 거부합니다. 풀을 워크로드에 맞게 크기를 정하고(CPU 바운드 작업에는 대략 코어 수, 스레드가 대부분 기다리는 I/O 바운드 작업에는 더 높게), 한계를 의도적인 용량 결정으로 다루십시오. 이는 3.3장의 복원력 패턴과 연결됩니다.

창피할 정도로 병렬적인 일에는 데이터 병렬성을 쓴다

어떤 문제는 깔끔하게 쪼개집니다. 어떤 요소도 다른 요소에 의존하지 않고 큰 데이터셋의 모든 요소에 같은 연산을 적용하는 것입니다. 이 데이터 병렬성은 경쟁할 공유 상태가 거의 없고 속도 향상이 코어 수에 가까워질 수 있어서 가장 친화적인 종류이며, 맵-리듀스 파이프라인, 병렬 배열 연산, 벡터화된 수치 코드가 모두 보여 줍니다. 여기서도 암달의 법칙을 존중하십시오. 병합이나 리듀스 단계는 직렬인 경우가 많아 이득에 상한을 두고, 작은 입력에서는 분할의 오버헤드가 지배할 수 있습니다. 요소별 작업이 상당하고 요소가 정말 독립적일 때 손을 뻗으십시오. 그렇지 않으면 가장 단순한 순차 버전이 충분히 빠르고 올바르게 유지하기 훨씬 쉬운 경우가 많으며, 이는 2.9장의 구축 관행이 강화하는 요점입니다.

비결정적 코드를 의도적으로 테스트하고 디버깅한다

동시성 버그는 비결정적이어서, 하나의 인터리빙을 실행하는 일반 테스트는 대부분 놓칩니다. 무작위 타이밍 아래 많은 작업을 실행해 드문 순서를 흔들어 내는 스트레스 및 퍼즈 테스트로 문제를 의도적으로 공략하십시오. 버그를 일으키는 인터리빙이 이번 실행에 일어나지 않았어도 데이터 경쟁을 잡도록 메모리 접근을 계측하는 도구인 경쟁 탐지기와 스레드 새니타이저에 손을 뻗으십시오. 플랫폼이 제공하면 특정 인터리빙을 재생하는 결정적 시뮬레이션이나 통제된 스케줄러를 써서 하이젠버그를 재현 가능하게 만들고, 프로덕션에서 멈췄을 때 스레드 상태와 락 소유권을 포착할 수 있게 설계하십시오. 이는 2.15장의 디버깅 규율과 연결됩니다. 무엇보다 이런 버그의 범주 전체를 불가능하게 만드는 설계(불변성, 메시지 전달, 단일 소유권)를 선호하십시오. 만들 수 없는 버그는 디버깅할 필요가 없기 때문입니다.

장단점

접근 방식장점단점
락을 쓰는 공유 메모리연산당 빠름. 익숙함경쟁, 교착, 가시성 버그. 많은 작성자가 올바르게 유지하기 어려움
불변성동기화 불필요. 사소하게 스레드 안전한 읽기복사 비용. 큰 가변 구조에는 어색함
메시지 전달 (액터, 채널)명시적 데이터 흐름. 버그의 부류 전체가 사라짐메시지당 오버헤드. 큐에 한계가 없으면 백프레셔를 가릴 수 있음
Async/awaitI/O 바운드 일에 싼 동시성. 순차적으로 보이는 코드CPU 일에는 병렬성 없음. 작업을 막으면 다른 것이 멈춤
구조적 동시성분명한 작업 수명. 오류가 전파됨. 새는 작업 없음더 새롭고 일부 생태계에서는 덜 이용 가능
데이터 병렬성독립적인 일에서 거의 선형 속도 향상암달의 상한. 작은 입력에서는 오버헤드가 지배
원자 / 락프리단순한 값에 락 경합 없음미묘하게 틀리기 극히 쉬움. 리뷰하기 어려움

핵심 트레이드오프는 안전 대 순수한 속도이며, 해법은 정확성을 먼저 사고 측정이 반드시 그래야 한다고 증명하는 곳에서만 성능을 쓰는 것입니다. 날 것의 공유 메모리 잠금은 연산당 가장 빠르고 코드 줄당 가장 위험합니다. 더 높은 수준의 모델은 약간의 처리량을 쓰고 많은 안전과 명료함을 돌려주며, 많은 손이 유지하는 코드에서는 그 거래가 결정적으로 할 가치가 있습니다. 손으로 조정한 락프리 동시성은 프로파일러(2.16장)가 조율 오버헤드가 중요함을 증명하는 작은 핫스팟에 남겨 두고, 그것조차 잘 테스트된 경계 뒤에 두십시오.

팀과 논의할 질문

  1. 가장 바쁜 서비스에서 워크로드는 I/O 바운드입니까 CPU 바운드입니까, 동시성 설계가 그에 맞습니까? 팀은 시간의 95%를 데이터베이스를 기다리는 서비스에 스레드 풀을 일상적으로 추가해 처리량 없이 경합만 얻거나, 직렬 비율이 어떤 속도 향상에도 상한을 두는 계산을 병렬화하려고 합니다. 올바른 설계는 체제에서 따라옵니다. 기다림이 많은 일에는 async나 작은 풀, 계산이 많은 일에는 코어에 걸친 진짜 병렬성입니다. 가정이 아니라 시간이 실제로 어디로 가는지 보여 주는 프로파일을 가져오십시오. 대부분의 시간이 계산에 쓰인다면 직렬 비율을 측정하고 암달의 법칙이 상한을 알려 주게 하십시오. 답이 async, 한계 있는 풀, 데이터 병렬성 중 무엇에 손을 뻗을지를 정합니다.

  2. 작업 사이에 상태를 공유하는 팀의 기본값은 무엇이며, 구조상 안전합니까? 큰 팀에서 기본값이 예외보다 더 중요합니다. 대부분의 코드가 동시성 전문가가 아니고 이미 있는 패턴을 복사하는 사람들이 쓰기 때문입니다. 기본값이 임시방편의 락으로 지켜지는 공유 가변 객체라면, 잊어버린 락 하나가 몇 달 뒤 프로덕션에서 표면화되는 경쟁 상태로부터 떨어져 있을 뿐입니다. 기본값이 불변성과 메시지 전달이면 버그의 범주 전체가 일어나지 않고, 정말 공유 메모리가 필요한 드문 곳은 주의 깊은 리뷰를 위해 눈에 띕니다. 새 엔지니어가 오늘 무엇에 손을 뻗을지, 리뷰가 동기화되지 않은 쓰기를 잡을지, 안전한 경로를 쉬운 경로로 만드는 방법을 논의하십시오.

  3. 프로덕션에서 백만 번에 한 번 나타나는 동시성 버그를 어떻게 찾고, 재현하고, 고치겠습니까? 많은 팀의 정직한 답은 할 수 없다는 것입니다. 보는 순간 버그가 사라지고 테스트는 하나의 무해한 인터리빙만 실행하기 때문입니다. 이는 걱정스러워야 합니다. 이런 버그는 데이터를 조용히 손상시키고 신뢰를 침식하기 때문입니다. 지속적 통합에서 경쟁 탐지기와 스레드 새니타이저를 실행하는지, 무작위 타이밍으로 스트레스 테스트하는지, 프로덕션 관측 가능성이 멈춘 순간의 스레드와 락 상태를 포착하는지 논의하십시오. 최고의 팀은 모델 선택으로 그런 버그 대부분을 불가능하게 만들어서, 남은 소수가 드물고 억제되게 하는 것으로 답합니다.

  4. 시스템 어디에 아직 한계 없는 큐나 상한 없는 스레드 풀이 있으며, 갑작스러운 열 배 급증 때 그것에 무슨 일이 일어납니까? 이것이 중요한 이유는 한계 없는 진행 중인 작업이 수수께끼 같은 메모리 부족 크래시로 가장하는 실패이기 때문입니다. 작업이 빠지는 것보다 빨리 도착하고, 메모리가 차고, 서비스는 과부하가 아니라 하드웨어 결함처럼 보이며 죽습니다. 상충하는 고려는 실제입니다. 너무 낮게 설정한 한계는 정당한 트래픽을 거부하고 너무 높은 한계는 크래시를 막지 않고 미루므로, 숫자는 추측이 아니라 용량 결정입니다. 모든 큐와 풀의 목록, 현재 한계(또는 없다는 인정), 가득 찼을 때의 백프레셔 동작, 가장자리에서 시스템이 어떻게 저하되는지에 대한 부하 테스트 증거를 가져오십시오. 기업 플릿에서 한계 없는 큐 하나가 플릿 전체의 장애로 연쇄할 수 있고, 시민에게 계속 이용 가능해야 하는 정부 플랫폼에서는 분명한 오류와 함께하는 우아한 거부가 서비스 의무이므로, 한계와 거부 경로는 한 엔지니어의 기억이 아니라 용량 계획과 런북에 속합니다.

  5. 더 높은 수준의 동시성 모델 대 손으로 쓴 락에 대한 팀의 정책은 무엇이며, 어디에 예외를 허용했습니까? 기본 모델이 평균적인 변경이 얼마나 안전한지를 결정합니다. 대부분의 작성자가 동시성 전문가가 아니고 이미 있는 패턴을 복사하기 때문입니다. 액터, 채널, 구조적 동시성은 모두의 바닥을 끌어올리는 반면, 날 것의 잠금은 이론상 올바르고 실제로는 교착의 원천입니다. 긴장은 더 높은 수준의 모델이 메시지나 작업당 약간의 오버헤드를 지고, 프로파일러가 가끔 핫 패스가 손으로 조정한 락프리 코드를 필요로 함을 증명할 것이므로, 전면 금지도 무법도 틀리다는 점입니다. 안전한 기본값 아래로 내려간 곳의 목록, 각각을 정당화한 프로파일링 증거, 각 예외가 테스트된 경계와 문서화된 락 순서 뒤에 어떻게 가둬졌는지를 가져오십시오. 큰 기업에서 이 정책은 수천 명의 기여자가 각자 안전하지 않은 방식을 재발명하지 않게 하는 것이고, 오래 사는 정부 시스템에서는 여러 해 뒤의 리뷰어가 위험한 패턴이 왜 허용되었는지 이해하고 여전히 정당한지 확인할 수 있게 하는 것입니다.

  6. 계산을 병렬화하기로 결정할 때, 직렬 비율을 어떻게 측정하며, 속도 향상이 실제임을 확인할 책임은 누구에게 있습니까? 팀은 계산을 코어에 퍼뜨리고 프로파일러가 결코 확인하지 않을 숫자를 자축하는 일이 일상적입니다. 암달의 법칙이 코어를 아무리 더해도 이득의 상한을 직렬 비율의 역수로 정하고, 분할-병합 오버헤드가 작은 입력에서는 이득을 완전히 지울 수 있기 때문입니다. 상충하는 끌림은 병렬성이 실제 복잡성과 새로운 경쟁 표면을 더하므로, 측정된 속도 향상이 떠안는 정확성 위험을 정당화하는지가 질문이라는 점입니다. 직렬 부분을 분리한 프로파일, 병렬성이 실제로 이기는 입력 크기, 희망적 추정이 아닌 대표적 하드웨어에서의 전후 벤치마크를 가져오십시오. 큰 컴퓨팅 플릿에 비용을 내는 기업에서 정직한 직렬 비율 분석은 절약되거나 낭비된 하드웨어 지출로 바뀌고, 공공 시스템의 비용에 책임지는 정부 기관에서는 병렬 설계를 승인한 사람이 감사 아래서 그것을 정당화한 측정을 보일 수 있어야 합니다.

분야별 관점

스타트업. 작은 팀과 여유 없는 런웨이라면 고용할 수 없는 동시성 전문가가 아니라 구조로 정확성을 사십시오. 언어가 주는 단일 안전한 기본값, I/O 바운드 일에는 async/await, 모든 공유 상태에는 하나의 소유 작업이나 액터에 손을 뻗고, 손으로 조정한 잠금은 완전히 건너뛰십시오. 결제 경로의 유실 갱신 경쟁 상태는 기능 하나를 놓치는 것보다 빨리 여러분을 침몰시킬 수 있으니, 그 부류의 버그를 불가능하게 만드는 약간의 추가 코드를 쓰고 넘어가십시오.

소기업. 동시성이 직무인 사람이 없으니, 대신 처리해 주는 플랫폼과 관리형 서비스를 선호하십시오. 데이터베이스 트랜잭션, 호스팅 큐, 프레임워크의 요청 모델이 손으로 유지하는 스레드를 이깁니다. 도구를 평가할 때 “이것이 동시성을 기본적으로 안전하게 만드는가”를 구매 대 개발의 질문으로 다루고, 잘못된 인터리빙이 고객의 레코드를 조용히 손상시킬 수 없는 선택지를 선호하십시오. 한계가 있는 관리형 서비스가 대신 쥘 수 있는 곳에서는 공유 가변 상태를 자체 코드 밖에 두십시오.

대기업. 많은 팀에 걸쳐 목표는 수천 명의 기여자를 안전하게 지키는 사내 기본값입니다. 규범으로서의 불변성과 메시지 전달, 날 것의 잠금보다 높은 수준의 모델, 백프레셔가 있는 한계 있는 큐와 풀, 문서화된 전역 락 순서입니다. 이를 엔지니어링 표준에 부호화하고, CI의 경쟁 탐지기와 스트레스 테스트로 시행하고, 프로파일러가 락프리 코드를 정당화한 예외를 다스려 각각이 테스트되고 검토된 경계 뒤에 머물게 하십시오. 동시성 용량을 측정된 부하에 묶인 큐 한계와 풀 크기로 플릿 전체의 관심사로 관리하십시오.

정부. 수십 년 운영되는 시스템에서 정확성과 감사 가능성이 순수한 처리량보다 중요합니다. 모든 상태 전이가 기록되고 재생 가능하도록 요구해, 의심되는 경쟁 상태를 재현하고 수정을 감독 기관에 증명할 수 있게 하고, 급여, 안전, 공공 기록을 건드리는 결정에는 AI 없는 결정적 경로를 유지하십시오. 조달은 벤더가 동시성 모델과 경쟁 탐지기 및 스트레스 테스트 커버리지의 증거를 공개하도록 요구해야 합니다. 공공 시스템에서 부하 아래의 잘못된 답은 불편이 아니라 책임 있는 공무원이 나중에 설명해야 하는 것이기 때문입니다.

사례

스타트업. 한 작은 팀이 결제 기능을 출시했는데 계정 잔액이 부하 아래서 가끔 몇 센트씩 어긋난다는 것을 알아챕니다. 원인은 동시 요청 핸들러가 잔액 필드에 하는 단순한 읽기-수정-쓰기, 유실 갱신 경쟁 상태입니다. 락을 뿌리는 대신, 각 계정의 잔액을 차변과 대변을 메시지로 한 번에 하나씩 처리하는 단일 소유 작업 뒤로 옮깁니다. 어긋남이 사라지고, 코드는 추론하기 쉬워지고, 수정을 지키려고 수천 건의 동시 이체를 쏘는 스트레스 테스트를 추가합니다. 하나의 구조적 변경으로 버그의 부류 전체가 은퇴했습니다.

대기업. 초당 수만 건의 요청을 처리하는 고처리량 주문 서비스가 트래픽 급증 중에 주기적 지연 급증과 가끔의 메모리 부족 크래시를 겪습니다. 조사 결과 수요가 용량을 넘으면 한없이 커지는 스레드 풀 뒤의 한계 없는 작업 큐가 원인입니다. 팀은 큐에 한계를 두고, 풀을 코어 수에 묶인 크기로 상한을 두고, 초과 부하를 분명한 오류와 함께 빠르게 거부하는 백프레셔를 추가합니다. 처리량이 예측 가능해지고 크래시가 멈추며, 공유 캐시의 뜨거운 락은 프로파일러가 경합이 실제임을 증명한 뒤에야 락프리 구조로 교체됩니다. 많은 작성자에게는 안전한 기본값, 측정된 곳에서만 조정된 동시성입니다.

정부. 한 국가 급여 플랫폼이 수십 년 운영되며 동시 사건 갱신 아래서도 감사 가능하고 올바른 결과를 내야 합니다. 팀은 불변성과 메시지 전달을 사내 기본값으로 고르고, 모든 가변 상태를 단일 소유자에 가두고, 락이 남은 곳에는 전역 락 순서를 부과하며, 모두 엔지니어링 표준에 써넣습니다. 파이프라인에서 스레드 새니타이저와 무작위 스트레스 테스트를 실행하고, 모든 상태 전이가 감독을 위해 기록되고 재생 가능하도록 설계해, 드문 인터리빙이 의심될 때 재현하고 수정을 증명할 수 있습니다. 정확성과 감사 가능성은 성능의 사후 생각이 아니라 일급 요구사항으로 다뤄집니다.

비즈니스 사례: 동기, ROI, TCO

규율 있는 동시성의 수익은 일어나지 않은 인시던트로 나타납니다. 단일 프로덕션 경쟁 상태가 수천 개 레코드에 걸쳐 데이터를 손상시킬 수 있고, 비용에는 관찰하면 숨는 버그를 찾는 엔지니어링 시간과 나쁜 데이터를 대사하고, 영향받은 사용자에게 알리고, 신뢰를 재건하는 훨씬 큰 비용이 모두 포함됩니다. 이것은 비결정적이어서 진단하기 가장 비싼 결함에 속하므로, 하이젠버그 하나를 쫓는 것은 안전한 모델을 앞서 고르는 노력을 압도할 수 있습니다.

이점은 처리량과 비용으로도 나타납니다. 동시성을 적정 크기로 잡으면 서비스가 같은 하드웨어에서 훨씬 많은 부하를 처리할 수 있어 큰 플릿에 반복되는 절감이 되고, 백프레셔와 한계 있는 큐는 트래픽 급증을 공개 인시던트로 바꾸는 연쇄 장애를 막습니다. 총소유비용은 소소하고 대부분 문화적입니다. 사내 스타일(불변성, 메시지 전달, 구조적 동시성), 도구(CI의 경쟁 탐지기, 스레드 새니타이저, 스트레스 하네스), 락 순서와 한계를 부호화하는 표준에 투자합니다. 대안은 정확성이 모든 작성자가 영원히 전문가이기를 요구하는 코드베이스이며, 어떤 성장하는 팀도 이를 지속할 수 없습니다. 리더십에게 그들의 단위로 논거를 세우십시오. 막은 경쟁 상태를 피한 데이터 손상 인시던트로, 백프레셔를 막은 장애로, 안전한 기본값을 절약한 온보딩 시간으로 옮기십시오.

안티패턴과 함정

  • I/O 바운드 일에서 속도를 위해 스레드 추가. 기다림이 많은 서비스의 더 많은 스레드는 처리량이 아닌 경합을 삽니다.
  • 곳곳의 공유된 가변 상태. 어떤 스레드든 어떤 객체든 바꾸면 정확성이 어떤 리뷰어도 검증할 수 없는 운의 문제가 됩니다.
  • 한계 없는 큐와 풀. 급증이 메모리가 죽을 때까지 큐를 키웁니다. 크래시는 수수께끼처럼 보이지만 단순한 과부하입니다.
  • 전역 순서 없는 임시방편 잠금. 코드베이스 전반에서 다른 순서로 잡힌 락은 부하 아래서 교착됩니다.
  • 코드가 쓴 순서대로 실행된다는 가정. 메모리 모델을 무시해, 가시성 버그가 스레드를 낡은 값에서 돌게 둡니다.
  • 손으로 만든 락프리 영리함. 자체 락프리 방식은 거의 항상 미묘하게 틀리고 리뷰하기가 거의 불가능합니다.
  • 행복한 인터리빙만 테스트. 결정적 테스트가 통과하는 동안 백만 번에 한 번의 순서가 프로덕션을 손상시킵니다.
  • 락을 쥔 채 알 수 없는 코드 호출. 막거나 재진입하는 콜백이 임계 구역을 교착으로 바꿉니다.

성숙도 모델

  • 1단계, 시작: 동시성이 그때그때 이루어지고 반응적입니다. 스레드와 락이 본능으로 추가되고, 공유된 가변 상태가 곳곳에 있고, 큐에 한계가 없습니다. 경쟁 상태는 아무도 진단할 수 없는 재현 불가능한 프로덕션 인시던트로 나타나고, 그것을 잡을 도구가 없습니다.
  • 2단계, 발전: 일부 팀이 기본 관행을 익혔습니다. 락을 더 조심스럽게 쓰고 가장 분명한 큐에 한계를 둡니다. 경쟁과 교착에 대한 비공식적 인식이 있고, 몇몇 핵심 경로가 추가 정밀 검토를 받습니다. 관행은 팀마다 일관되지 않고, 테스트는 여전히 대부분 단일 인터리빙이며, 안전한 패턴은 서면이 아니라 개인에 삽니다.
  • 3단계, 표준화: 조직에 조직 전체에서 시행되는 문서화된 사내 스타일이 있습니다. 기본값으로서의 불변성과 메시지 전달, 날 것의 잠금보다 높은 수준의 모델, 백프레셔가 있는 한계 있는 큐와 풀, 문서화된 전역 락 순서입니다. 경쟁 탐지기와 스트레스 테스트가 CI에서 실행되고, 동시성 선택은 일이 I/O 바운드인지 CPU 바운드인지에서 따라옵니다.
  • 4단계, 관리: 조직이 동시성 태세를 기준선에 대해 측정하고 통제합니다. 서비스 전반의 경쟁 탐지기와 스레드 새니타이저 커버리지를 추적하고, 큐 깊이, 락 대기 시간, 풀 포화, 거부율을 모니터링되는 지표로 기록하고, 저하 곡선을 부하 테스트하여 모든 한계가 데이터에 근거한 용량 결정이 되게 합니다. 동시성 인시던트는 집계되고 추세화되며, 병렬화된 워크로드의 직렬 비율은 실제 달성된 속도 향상과 대조 측정되고, 새 설계에 대한 가부 결정은 본능이 아닌 그 증거에 근거합니다.
  • 5단계, 오케스트레이션: 안전한 동시성이 모든 작성자에게 최소 저항의 길이며, 실천이 지속적으로 개선되고 조직 전체에 통합됩니다. 버그의 부류 전체가 구성상 불가능하고, 핫스팟은 프로파일링이 증명하는 곳에서만 조정되며, 결정적 재생이 드물게 남은 버그를 재현 가능하게 합니다. 정확성과 감사 가능성은 지속적으로 방어되는 속성이고, 용량 한계는 관찰된 부하에 적응하며, 표준은 플랫폼과 워크로드가 이동함에 따라 진화합니다.

논의를 위한 아이디어

  1. 오늘 가장 바쁜 서비스를 감사한다면, 그 상태의 얼마가 공유되고 가변이며, 그 공유의 얼마가 진정 필요합니까?
  2. 두 작업이 조율해야 할 때 팀의 기본 답은 무엇이며, 그것이 불변성이나 메시지 전달이기를 바랍니까?
  3. 시스템 어디에 아직 한계 없는 큐나 상한 없는 풀이 숨어 있으며, 갑작스러운 열 배 트래픽 급증 때 그것들에 무슨 일이 일어나겠습니까?
  4. 지속적 통합 실행에 경쟁 탐지기나 스레드 새니타이저가 포함되며, 프로덕션 전에 마지막으로 무언가를 잡은 것은 언제입니까?
  5. 가장 병렬화된 워크로드의 직렬 비율은 얼마이며, 암달의 법칙이 실제로 쫓는 속도 향상에 상한을 둡니까?
  6. 팀이 백만 번에 한 번의 인터리빙 버그를 요청 시 재현할 수 있겠으며, 거기에 이르려면 무엇이 필요합니까?

핵심 요점

  • 동시성은 프로그램을 독립적인 작업으로 구조화하고, 병렬성은 그것을 한꺼번에 실행합니다. 스레드를 추가하기 전에 무엇이 필요한지 정하십시오.
  • 공유된 가변 상태가 거의 모든 동시성 버그의 뿌리입니다. 많은 작성자를 위한 안전한 기본값으로 불변성과 메시지 전달을 선호하십시오.
  • 이론상 올바르고 실제로는 위험한 손으로 쓴 락보다 먼저 높은 수준의 모델(액터, 채널, 구조적 동시성, async/await)에 손을 뻗으십시오.
  • 원자성, 가시성, 메모리 모델을 이해하십시오. 올바른 기본 요소를 쓰고, 락을 짧게 쥐고, 교착, 라이브락, 기아를 피하도록 전역 락 순서를 부과하십시오.
  • 모든 큐와 풀에 한계를 두고 백프레셔를 적용해 급증이 크래시가 아니라 우아하게 저하되게 하십시오(3.3장).
  • 경쟁 탐지기, 스트레스 테스트, 재생으로 인터리빙을 의도적으로 테스트하고(2.15장), 병렬화할 때 암달의 법칙을 존중하십시오(2.16장).
  • 기업에게 이는 처리량과 막은 인시던트이고, 정부에게는 오래 사는 시스템의 정확성과 감사 가능성입니다.

참고 문헌과 더 읽을거리

  • Brian Goetz et al., Java Concurrency in Practice (atomicity, visibility, the memory model, and safe publication).
  • Herb Sutter, “The Free Lunch Is Over” (why software must embrace concurrency as clock speeds plateau).
  • Leslie Lamport, “Time, Clocks, and the Ordering of Events in a Distributed System” (ordering and the foundations of concurrent reasoning).
  • C. A. R. Hoare, “Communicating Sequential Processes” (Communications of the ACM, 1978): the CSP model behind channels.
  • Carl Hewitt, Peter Bishop, and Richard Steiger, “A Universal Modular Actor Formalism for Artificial Intelligence” (the origin of the actor model).
  • Edsger W. Dijkstra, “Cooperating Sequential Processes” (semaphores, mutual exclusion, and the deadlock problem).
  • Maurice Herlihy and Nir Shavit, The Art of Multiprocessor Programming (locks, atomics, and lock-free data structures).
  • Nathaniel J. Smith, “Notes on Structured Concurrency, or: Go Statement Considered Harmful” (the case for structured concurrency).
  • Martin Kleppmann, Designing Data-Intensive Applications (concurrency and consistency where memory meets distributed systems).
  • Gene M. Amdahl, “Validity of the Single Processor Approach to Achieving Large-Scale Computing Capabilities” (1967): the origin of Amdahl’s law.