나머지를 배반하지 않고 병행 처리를 설계하기
병행 처리는, 언어가 그것을 가져야 하기 때문이 아니라, 진짜를 만드는 중에 부딪힌 벽이 강요했다 —— 하나의 프로세스를 공유할 수 없는 두 영구 loop. 어려운 것은, 보장을 놓지 않고 그것을 더하는 것이고, 내기는, 메모리와 이펙트를 위해 이미 세운 기구 —— 소유권, move, Trivial —— 이야말로 안전한 병행 처리에 드는 것이고, 그것을 스레드 경계로 확장한 것이다, 라는 것이다.
공에서 도는 언어는, 사적인 것이 미룰 수 있던 물음에 직면한다: 다음에 무엇을 키우는가? Mere에게 그 답은 소망 목록에서 골라진 것이 아니라 —— 강요되었다. 진짜 프로그램을 세우는 중에 벽이 나타났고, 그것을 넘는 것은, 강한 보장을 가진 언어가 떠맡을 수 있는 가장 어려운 feature 중 하나 —— 병행 처리 —— 를 더하는 것을 뜻했다. 이 Part는, 앞 Part가 세운 모든 것을 배반하지 않고 그것을 더하는 것에 대한 것이다.
그것을 요구한 벽
pain point은 구체적이고 수수했다. HTTP 서버는 영구 loop다 —— 요청을 기다리고, 결코 돌아오지 않는다. 제2의 영구 loop도 필요한 프로그램 —— 메시지 채널을 듣는 subscriber, 백그라운드 워커, 스케줄된 잡 —— 은, 한 번에 돌아야 할 두 loop를 가지고, 그리고 Mere는 하나의 프로세스에서 두 loop를 돌리는 수단을 갖지 못했다. 당장의 회피책은, 제2의 loop를 JavaScript 호스트로 밀어내어, 그 event loop에 둘을 interleave시키는 것이었다. 그것은 돌지만, 고백이다: 언어가, 진짜 프로그램이 일상적으로 요하는 것을 표현할 수 없어, 그래서 수요가 둘레의 런타임으로 새어 나왔다.
그것은 가장 순수한 형태의 dogfood signal이다 —— 추상적으로 상상된 feature가 아니라, 쓸 수 없었던 구체적인 무언가, 그것을 쓰려다 발견된. 병행 처리는, 프로그램이 벽에 부딪혔기에 의제에 올랐다. 그것은 “언어는 스레드를 가지는 법이다”와는 전혀 다른 정당화다. 그 형태는 되풀이된다 —— 서버+워커, 서버+cron, 서버+텔레메트리 —— 그래서, 영원히 우회하는 것이 아니라, 제대로 풀 가치가 있었다.
어려운 부분: 보장을 지키기
병행 처리는, 언어의 안전성 약속이 슬며시 실효하는 고전적인 곳이다. 두 스레드가 가변 상태를 공유하는 순간, 데이터 경쟁을 상속한다 —— 타이밍에 의존하고, 소스에 보이지 않고, 대부분의 테스트 실행에 부재한 버그. 다섯 Part를 메모리와 이펙트를 명시적이고 검사 가능하게 하는 데 들인 언어가, 그 뒤 스레드를 더하고, 그것들이 무엇을 공유하는가에 어깨를 으쓱할 수는 없다. 이 Part의 어려움의 전부는, 병행 처리가 다른 모든 것만큼 명시적이고 검사되어야 한다는 데 있다.
Mere가 하는 내기는, 거기에 이르는 데 새로운 안전성 체제를 요하지 않는다는 것이다. 이미 가진 기구
야말로 옳은 기구이기 때문이다. 소유권과 move는 이미 “이 값은 이제 다른 누군가에 속하고, 당신은
건드릴 수 없다”를 모델화한다. 대여 표기는 이미 공유 읽기와 배타 쓰기를 구별한다. Trivial 제약은
이미, 정리 없이 다룰 수 있는 것을 분류한다. 안전한 병행 처리는, 내기에 따르면, 그 같은 아이디어를
스레드 경계를 넘어 나른 것이다: 다른 스레드에 건네진 값은 move된 값; 두 스레드가 공유하는 값은,
공유해 안전하다고 증명해야 한다, 타입 시스템이 하나의 스레드 안의 공유에 대해 이미 추론하는 것과 꼭
같이. 병행 처리는 소유권 이야기의 확장이 되고, 그 곁의 볼트 접합이 되지 않는다.
하나가 아니라 다섯 물음
“병행 처리를 더한다”는 정하기에 너무 막연하다. 그래서 그것은, 연재의 되풀이되는 방법으로, 저마다 답할 수 있는 다섯 정밀한 물음으로 분해되었다:
- 실행 모델 —— 진짜 OS 스레드, 경량 그린 스레드, 아니면 async 태스크인가?
- 경계를 넘기 —— 능력이 자식 스레드로 갈 때, 그것은 move되는가, 공유되는가, 복사되는가?
- 채널 —— 채널에 흘리기 위해, 타입에 대해 무엇이 참이어야 하는가?
- Send와 Sync —— “스레드 간을 move해 안전”과 “공유해 안전”의 trait 같은 마커가 필요한가, 필요하면 얼마나 무겁게?
- region과
with와의 상호작용 —— 자식 스레드는 자기 region을 얻는가, 정리 순서는 어떻게 되는가?
그리고 설계는, 이펙트 시스템이 그랬듯, 그것이 받아들여야 할 프로그램과 거부해야 할 것을 씀으로써 못 박혔다: 도는 병렬 계산; 부모가 더는 못 쓰도록 자식에 move된 logger(그리고 재사용은 컴파일 에러); 명시적으로 공유되어 양쪽에서 쓸 수 있는 logger; 넘겨 건네면 타입 에러가 되는 단일 소유자의 네트워크 연결; 채널에 허용되는 보낼 수 있는 타입만. 수용 기준은 산문이 아니라 프로그램이다.
좁힘
답은, 종이 위에서 좁혀지고, 그다음 진짜 구현 전에 작은 trial로 확인되어, 모두, 기존 설계와 정합하는 최소의 것으로 기울었다.
실행 모델은 진짜 OS 스레드 —— 구체적으로는, 호스트 언어 OCaml이 이미 제공하는 병렬성의 primitive. 그것은 최소의 구현이고, 실제 CPU 코어를 직접 쓰고, 자체 스케줄러를 요하지 않는다. async 태스크 모델은, 다른 데서는 우아하지만, 능력 전달의 결정에 역행한다고 하여 기각되었다 —— 그것은 바로, 이펙트 시스템이 없애기 위해 존재하는 암묵의 제어 흐름을 재도입한다. OS 스레드를 고르는 것은, 왜 OCaml인가의 내기가 다시 한 번 보답받는 것이다: 호스트의 스레드가 Mere의 스레드가 된다.
경계를 넘는 것은 기본으로 move —— 자식에 건네진 능력은 자식에 속하고, 부모가 그 뒤 그것을 쓰는
것은 컴파일 시간 에러 —— 이고, 자기 자신의 내부 락을 나르는 값에만 명시적 공유. 채널은 그 요소
타입이 보내 안전할 것을 요구한다. 그리고 “보내 안전”과 “공유해 안전”의 성질은, 완전한 trait 시스템을
세우는 것이 아니라, 두 구조적 술어 —— Send와 Sync —— 를, 언어가 이미 좇던 Trivial과 정리를
요하는 구별의 곁에 더함으로써 다뤄진다. 그것은 프로젝트의 다른 것과 같은 절제다: 물음에 답하는 최소의
기구, 일반적인 기계를 발명하는 것이 아니라, 존재하는 것을 재사용한다. 마지막으로, spawn된 자식은 자기
새 region을 얻어, 부모와 자식의 메모리가 alias하지 않고, region 모델의 보장이 스레드 경계를 무사히
살아남는다.
여기에 완성된 것은 아직 없다 —— 이것은 형태다, 좁혀지고 trial로 확인된, 구현이 아니라. 하지만 형태
야말로 중요한 커밋먼트다: Mere의 병행 처리는 확장된 소유권이지, 포기된 안전성이 아니다. 다음 회들이
그것을 세운다. 먼저, 설계 전체가 얹히는 두 술어가 실제로 돌아야 한다 —— 그리고 그것을 올바르게 하는
데에는, 미묘한 구멍이 있다고 판명되었다. 다음 회: Send와 Sync의 타입.