openlang

ECMA-262에는 이벤트 루프가 없다

ECMA-262에는 이벤트 루프라는 말이 한 번도 안 나와요. 명세가 정의하는 건 잡(job)과, 프로미스 반응이 실행될 준비가 됐을 때 엔진이 호출하는 HostEnqueuePromiseJob이라는 호스트 훅이에요. 그 잡을 실제로 언제 돌리고 그 사이에 뭘 돌릴지는 호스트가 정해요.

그래서 setTimeout은 언어 명세에 아예 없고, 타이머 콜백이 .then 콜백보다 먼저냐는 논쟁도 ECMA-262만 읽어서는 결론이 안 나요. 다음 타이머 태스크 전에 프로미스 잡을 전부 비운다는, 다들 믿고 쓰는 순서는 HTML 이벤트 루프와 Node에서 오는 거지 코어 명세에서 오는 게 아니에요.

그래도 쓸모 있는 구분이에요. 한 턴 안에서 프로미스 반응은 큐에 들어간 순서대로 돌고, 이미 resolve된 값에 await를 걸어도 잡 하나를 기다리기 때문에 그 뒤 코드가 동기적으로 실행되는 일은 없어요. Dart도 같은 두 단계 구조라서 scheduleMicrotask로 넣은 작업이 대기 중인 이벤트보다 먼저 돌아요.

호스트들은 꼭 그럴 필요가 없는 부분까지 서로 맞춰 줘요. 다만 명세가 그린 적도 없는 그림으로 시작하는 설명이 좀 줄었으면 좋겠어요.

이어지는 대화 2

서로의 근거를 따라
  1. 고유 링크

    이건 델타 사이클과 거의 일대일로 대응됩니다. VHDL에서 신호 대입은 즉시 반영되지 않습니다. 새 값을 예약할 뿐이고, 시뮬레이터는 깨어난 프로세스를 모두 끝까지 실행한 뒤 신호를 갱신하고 그 신호에 민감한 프로세스를 깨웁니다. 이 모든 과정에서 시뮬레이션 시간은 흐르지 않습니다. 더 처리할 델타 사이클이 없을 때만 시간이 진행됩니다.

    "신호 갱신"을 "프로미스 잡"으로, "시간 단계"를 "태스크"로 바꿔 읽으면 구조가 같습니다. 계속 작업을 큐에 넣는 .then 체인은 조합 루프가 시뮬레이터를 한 시점에 묶어 두는 것과 같은 방식으로 타이머를 굶깁니다. GHDL은 델타 사이클 한도에 닿으면 그런 실행을 멈춥니다. 브라우저 탭은 그냥 멈춰 버립니다.

    기억해 둘 차이도 하나 있습니다. 같은 델타에서 재개된 프로세스들은 실행 순서와 상관없이 모두 같은 신호 값을 읽습니다. 프로미스 잡은 서로의 부수 효과를 곧바로 봅니다.

  2. 고유 링크

    SystemVerilog에서는 비유가 더 가까워집니다. 한 타임 슬롯을 이름 붙은 영역으로 나누기 때문입니다. Active, Inactive, NBA가 있고, 그 뒤에 assertion과 program 블록을 위한 Observed와 Reactive가 있으며, 몇 개가 더 있습니다. 블로킹 대입은 Active에서 실행되고, #0은 프로세스를 Inactive로 옮기며, 논블로킹 <=의 갱신은 NBA까지 기다립니다.

    경쟁 상태를 피하려고 #0을 여기저기 뿌린 테스트벤치는 대기 중인 .then을 먼저 보내려고 await null을 쓰는 것과 같은 수입니다. 누군가 하나 더 넣기 전까지만 통합니다. 대신 clocking 블록으로 샘플링해서 클록 기준으로 정해진 지점에서 읽게 하십시오.