무언가를 만들지 않는다는 결정

완전한 병행 처리의 이야기는, atomic·mutex·read-write lock을 포함해야 한다. Mere의 로드맵은 그것들을 늘어놓았다 —— 그리고 일부러 만들지 않았다. 선택은 체크리스트가 아니라 수요로 이뤄졌다: 어느 프로그램이 실제로 그것들을 요하는가? 도는 example이 답했고, 그중 하나가 gap의 정확한 비용을 정량화하고 —— 그리고 첫 분석이 물리친 genuine한 case가 나타났을 때, 정직한 결론이 한 걸음 수정되었다.

mereconcurrencyroadmapyagnilanguage-design

병행 처리는 이제 돈다: 안전한 primitive, 인간적인 콤비네이터, 일치한 네 backend. 하지만 로드맵은 더 늘어놓았다 —— “완전한” 병행 처리의 이야기가 가진다고 기대되는, 더 저수준의 공유 상태 기구: atomic, mutex, read-write lock. 이 Part의 마무리 회는, 그것들을 만들지 않는다 —— 적어도 지금은 —— 고 정하는 것에 대해, 그리고 무엇을 만들지 않을지를 잘 정하는 것이 진짜 공학적 행위라는 사실에 대한 것이다. 체크리스트를 따르는 것이 아니라, 수요를 재는 것으로 이뤄진다.

체크리스트는 yes라 한다; 대신 수요가 물어졌다

병행 처리 시스템의 feature 체크리스트는, 주저 없이 atomic과 락을 늘어놓는다; 진지한 언어는 모두 그것들을 가진다. 더 유용한 물음은 더 좁다: 이미 존재하는 것을 감안하여, 어느 프로그램이 실제로 그것들을 요하는가? 도는 example이 답했고, 답은 대개 “이것들은 요하지 않는다”였다.

공유 가변 상태로 보이는 문제의 대부분은, 실은 map-reduce이고, channel이 이미 그것들을 덮는다. 술어를 만족하는 개수를 세는 것은 공유 카운터로 보이지만, 각 worker가 0이나 1을 반환하고 결과를 합칠 뿐이다 —— 락 없이, atomic 없이, backend를 넘어 변하지 않고 돈다. 진짜로 상태를 가진 문제는 actor로 쓸 수 있다: 하나의 worker가 상태를 소유하고, 다른 이는 channel로 그것에 request를 보내 reply를 얻는다 —— Erlang의 “통신함으로써 메모리를 공유하라”이고, Mere의 channel은 다대다이므로, work queue가 같은 기구에서 흘러 떨어진다. 그리고 read-only의 공유는, 새 feature 없이 이미 돈다: 불변한 값은 공유 가능 하므로, 여러 worker가 하나의 큰 테이블이나 config를 병행으로 capture해 read할 수 있다. 그 세 형태 —— reduce, actor, 공유 read —— 를 넘어, 기존의 primitive로 족하다.

그것이 정확한 잔여를 남긴다: atomic과 락이 genuine하게 효하는 것은 성능을 위해서뿐이다. 뜨거운 카운터는, actor로의 message보다 atomic increment가 훨씬 값싸다. read-heavy한 공유 상태는, 모든 access를 하나의 thread로 직렬화하는 actor보다, read를 병행으로 돌리는 read-write lock 뒤가 빠르다. 첫 판단이 따랐다: 기능적으로는 channel과 actor가 공유 상태를 완전히 덮으므로, 이 기구는 성능 최적화이고, 그것을 세울 옳은 때는, 실의 benchmark가 실의 bottleneck을 보일 때다 —— 그것은 또 API가 무엇이어야 하는지를 정확히 정의한다. 그때까지 defer. 그것은, 패키지 매니저를 defer하고, 더 일반적인 타입 시스템의 기구를 선반에 남긴 것과 같은 수요 구동의 규율이다: 아직 가리킬 수 없는 수요를 위해 세우지 말라.

정직한 수정

그 정돈된 결론 —— “순수히 성능, defer” —— 은 한 걸음 수정되었다. 더 어렵게 보는 것이, 그것이 물리친 case를 파냈기 때문이다. 병렬 알고리즘의 몇몇은 실행 중에 공유 가변 상태를 read하여 다음에 무엇을 할지를 정한다, 그리고 branch-and-bound 탐색이 그 clean한 예다: 탐색하는 대로, 주행 중의 best 결과를 유지하고, 그것을 써서 그것에 이길 수 없는 가지를 친다.

작은 example이 비용을 정확히 쟀다. 같은 최대화를 두 방식으로 —— 순차로, 하나의 공유된 주행 중 best로, 그리고 병렬로 네 worker를 넘어, 각자의 로컬 best로 —— 돌리면, 같은 답을 찾았지만, 아주 다른 비용으로: 순차판은 비싼 평가를 한 번, 병렬판은 네 번 했다. 병렬의 worker는, 서로의 주행 중 best를 값싸게 read하지 못해, 공유된 best가 건너뛰게 했을 것을 각자 평가했다. message passing은 옳은 답을 주지만, 치기를 유지할 만큼 값싸게 살아 있는 값을 공유하지 못하고, 그래서 알고리즘의 질이, 그 결과가 옳더라도 열화한다.

그것은 순수한 성능이 아니라, 알고리즘이 나빠지는 것이고, 이전의 결론이 너무 정돈되었음을 뜻했다. atomic으로의 genuine한 motivator가 결국 있다 —— branch-and-bound, 적응 탐색, 살아 있는 공유 budget. 정직한 정정은 양방향으로 동시에 달린다: case는 진짜이므로 “그저 성능의 편안함”은 걸어 물러설 과한 주장이었다; 하지만 그것은 또, 하드한 capability gap이 아니다, 병렬 프로그램은 여전히 옳은 답을 낳기에 —— 그래서 “이것을 표현할 수 없다”도 과한 주장일 것이다. 재어진 진실은 정확한 것이다: 오늘, 표현 가능 하고 옳다, 하지만 공유 atomic이 되찾을, 정량화된 치기의 손실을 동반하여.

영수증 붙은 defer

그래서 결정은 유지되었다 —— defer —— 하지만 제대로 이뤄진 defer는, drop과 같지 않다. gap을 찾은 example은, 그것을 닫을 것도 지정한다: read와 compare-and-update 조작을 가진 공유 atomic scalar, 스레드 간에 공유 가능이라 표시되고, 그리고 구조화된 공유 상태를 위한 read-write lock, 사용 패턴을 써서 (worker가 공유 best를 read해 치고, 이겼을 때 atomic하게 갱신). 트리거도 이름 붙여진다: branch-and-bound 나 적응 병렬 탐색이 실의 타깃이 될 때, 이것을 세운다. 이것은 영수증 붙은 미뤄진 물음이다 —— 그것이 없음의 비용은 재어지고, 그것이 요할 API는 초안되고, 그것을 un-defer해야 할 조건은 써진다. 그것이, 결정과 어깨를 으쓱하는 것의 차이다.

그리고 그것은, 연재 전체가 돌아온 규율의 조용한 정점이다. 몇 번이고, 답은 defer하는 것이었다 —— 패키지 매니저, 일반적인 제약 solver, 프로그램이 원할 때까지의 Wasm 스레드 —— 늘 같은 시험으로: 실의, 현재의 수요가 있는가, 아니면 추측된 미래의 것인가? 만들지 않는다고 정하는 것은, 결정의 부재가 아니다; 잘 이뤄지면, 그것은 측정이고, 초안된 답이고, 이름 붙여진 트리거이며, 그것은 종종 yes라 하는 것보다 더 많은 일이다. 언어는, feature를 거부함으로써가 아니라, 각 거부를 각 feature와 같은 만큼 세심히 그 자리를 벌게 함으로써, 작게 유지된다.

이것으로, 병행 처리 Part가 닫히고, 그와 함께 주요 시스템의 마지막이. 언어는 사상, 메모리 모델, 이펙트, 엄밀한 일치에 유지된 네 backend, 자기 자신으로 쓰인 컴파일러, 공적인 존재, 그리고 안전한 병행 처리를 가진다 —— 그리고, 그것이 어디서 일부러 멈추는가의, 명확하고 재어진 설명을. 남는 것은 시스템이 아니라 지평이다. 다음 회, 마지막 Part: 다음에 오는 것 —— 지도의 가장자리의 feature와, 그것들이 세워지는 조건.

← Back to Mere: 언어를 만들다