멈출 수 없는 worker
worker pool —— main이 jobs를 쌓고, workers가 pull해 처리하고, main이 회수한다 —— 이 같은 1분 안에 두 개의 벽에 부딪혔다. worker의 수신 루프는 jobs가 다하면 영원히 블록하고, 멈춰서 join할 수단이 없다. 그리고 루프가 결코 돌아오지 않으므로 타입은 bottom이 되어, C backend는 애초에 emit할 수 없다. 두 벽은 같은 결락 primitive였다: channel에는 「끝났다」고 말할 수단이 필요하고, 수신에는 그것을 들을 수단이 필요하다.
메모리 모델 이야기들은 「의도적으로 남긴 것」의 목록으로 끝났고, 그 필두가 구조화 병행이었다. 언어는 스레드를 spawn하고 메시지를 넘길 수 있었지만, 병행 프로그램이 어떻게 끝나는가라는 물음을 강제하는 것이 없었다. 이 Part는 드디어 그것을 강제한 아크다 —— 하루 세 개의 작은 프로그램, 각각이 「끝」 문제의 한 조각을 겨눈다. 전체를 꿰뚫는 테마: 병행 프로그램의 난소는 움직이게 하는 것이 아니다. 멈추게 하는 것이다.
1분 차이의 두 벽
forcing program은 병행 프로그래밍에서 가장 흔한 형태로 했다: worker pool. main이 N개의 jobs를 channel에 쌓고, W개의 workers가 각각 job을 pull해 결과를 push하는 루프를 돌고, main이 N개의 결과를 회수한다. 쓰자마자 거의 즉시 두 개의 벽에 부딪혔다. 첫째는 의미론의 벽: jobs가 다하면, 각 worker는 두 번 다시 값을 낳지 않는 블로킹 수신 안에 주차한다 —— worker에게 「더 이상 일이 없다」고 전할 수단이 없으므로, 멈출 수단이 없고, join할 수단이 없고, 프로그램은 깨끗이 끝날 수 없다. 둘째 벽은 무관해 보이지만, 컴파일러의 얼굴을 한 같은 벽이다: 결코 돌아오지 않는 worker 루프는 bottom 타입을 가진다 —— 타입 검사기의 「이 식은 아무것도 낳지 않는다」는 말투 —— 그리고 C backend는 그 타입 값의 emit을 단적으로 거부한다. 프로그램은 실행 시에 행 걸리기 이전에, 컴파일되지 않았다. 하나의 결락 primitive가, 한 번은 운용 문제로, 한 번은 타입으로 보였다.
channel을 닫는다
그 primitive는 큐 의미의 닫힘이다: **channel_close**는 channel을 「끝났다」고 표시하고,
**channel_recv_opt**는 값을 블록 대기하지만, channel이 closed이면서 drained가 되면 None을
반환한다. worker의 루프는 match가 된다: Some job —— 처리하고 루프, None —— return. 루프는
이제 종단하고, 그것이 모든 것을 한 번에 고친다: worker는 unit을 반환하고(bottom이 아니므로
컴파일된다), pool은 handle마다 join할 수 있고, 프로그램은 안의 모든 스레드가 끝났기에 끝난다.
closed된 channel로의 송신은 raise한다 —— 그것은 프로그래밍 에러지, 비틀대며 계속할 상황이
아니다 —— 그리고 closed & drained로의 종래 블로킹 수신도, 아무도 signal하지 않는 조건 변수
위에서 영원히 자는 대신 raise한다. 스코프는 의도적이다: interpreter와 C backend —— 서버형
dogfood가 실제로 도는 곳 —— 에 구현하고, 브라우저 쪽 두 backend는 그 취지를 말하는 컴파일
에러로 거부한다. pool의 example은 4 workers로 12 jobs를 돌려 합산하고, 그리고 —— 전에는
불가능했던 부분 —— 4 worker 전부를 join하고 exit한다. 다음 이야기는 이 이야기가 구하지 못하는
worker를 만난다: 아직 돌고 있으면서, 그저 끝나기를 거부하는 놈이다.