OpenAI Codex · 도구 스케줄

모델에게는 마음대로 병렬하라 하고, 아래는 잠금으로 막아요

모델이 보는 것, 실제로 도는 것, 이력에 적히는 것은 순서 셋이에요. 깃발 하나가 한 응답에 호출을 여러 개 보내게 하고, 읽기-쓰기 잠금 하나가 누가 겹쳐 들어갈지를 정합니다.

강의 목표읽고 나면 세 가지를 말할 수 있어요. 모델에 보내는 병렬 깃발은 한 응답에 도구 호출이 여러 개 나와도 된다는 뜻일 뿐이에요. 도구마다 기본은 쓰기 잠금이고, 병렬이려면 직접 고쳐야 합니다. 결과를 세션에 쓸 때는 모델이 처음에 보낸 순서로 줄 세워요.
먼저 해보기 · 도구 몇 개가 같이 출발
읽기-쓰기 잠금 하나: 누가 메뉴를 보고, 누가 문 밖에서 주방을 기다리나
장면
칸을 누르면 읽기 패와 쓰기 패를 바꿀 수 있어요. 늦게 온 읽기는 fair 잠금을 보여 줍니다. 쓰기 쪽이 이미 줄을 섰으면, 뒤따라 온 읽기는 새치기할 수 없어요.
메뉴 · 읽기 잠금
병렬인 것은 여럿이 같이 있을 수 있어요
주방 · 쓰기 잠금
독점, 한 명만
문 밖 대기
모델이 보낸 순서
실제로 돈 순서
이력에 기장된 순서
시작을 기다려요. 재생을 눌러 잠금이 어떻게 갈라내는지 보세요.
논리 궤적 · 애니메이션 한 걸음이 소스 어디에 대응하는지
  1. build_prompt가 병렬 깃발을 true로 씀turn.rs L1321
  2. 요청을 짤 때 Lite 표시와 ANDclient.rs L952
  3. 라우터가 레지스트리를 찾고, 없으면 falserouter.rs L137
  4. Hidden은 스스로 병렬이라 해도 직렬registry.rs L472
  5. 먼저 spawn, 준비될 때까지 기다린 뒤 잠금을 잡음parallel.rs L144
  6. 병렬이면 read, 아니면 writeparallel.rs L152
  7. 결과는 FuturesOrdered 삽입 순으로 기장turn.rs L2135
재생을 누르세요. 도구 몇 개가 같이 출발한 뒤, 누가 겹쳐 들어가고 누가 기다려야 하는지 봐요.
누가 병렬인가읽기 패를 켠 것은 메뉴로 들어가 겹쳐 달릴 수 있어요. 쓰기 패를 켠 것은 주방을 독점하고, 문 밖에 쓰기 쪽이 있으면 뒤따라 온 읽기도 그 뒤에 서야 합니다.
순서 셋모델이 보낸 순서는 1, 2, 3, 4예요. 실행 순서는 겹칠 수 있어요. 이력 기장은 여전히 보낸 순서라, 먼저 보낸 것이 먼저 기장되고, 더 늦게 끝나도 그렇습니다.
당신이 바꿀 수 있는 경계패치를 읽기 패로 바꾸면 넷이 같이 들어가요. 읽기 패를 쓰기 패로 바꾸면 서로 막습니다. 늦게 온 읽기는 fair 잠금이 새치기를 안 시킨다는 걸 보여 줘요.
수업용 시연: 칸과 소요 시간은 수업용 설정이에요. 읽기-쓰기 잠금이 갈라내는 모습과, 순서 셋이 어긋날 수 있음을 보여 주려고요. 논리 궤적 오른쪽 행 번호는 openai/codex 저장소 commit 4f39251a01에 맞춰 있어요.
아이디어 1 · 모델에게는 병렬이라 하고, 아래는 스스로 갈라내요
어떤 문제를 푸는가

모델에게 src/main.rssrc/lib.rs를 같이 읽고, 패치 한 곳을 치라 해요. 화면에서는 읽기 둘이 거의 같이 진행되고, 패치 치는 그 박자만 한숨 멈춥니다.

런타임이 “병렬 가능”을 이 호출들이 겹쳐 달린다는 뜻으로 받으면, apply_patch 둘이 공유 diff 장부를 같이 고쳐 장부가 엉망이 돼요. 호출을 머리-꼬리로 이으면 파일 읽기 둘도 줄을 서서 벽시계 한 라운드를 더 씁니다. 요청 쪽 깃발은 모델이 상자 여러 개를 한 번에 보낼 의지가 있는지만 답하고, 상자가 바닥에 떨어질 때 누가 겹칠지는 답하지 못해요.

아이디어는 무엇인가

모델에 보내는 깃발은 Prompt에 있어요. 필드는 기본 false이고, 샘플 주 경로는 build_prompt를 타 깃발을 true로 씁니다. Responses 본문을 짤 때 모델이 Responses Lite인지와 한 번 AND해요. Lite에서는 이 깃발이 꺼집니다. 압축 묶음 경로 둘도 true로 박아, 주 샘플 요청 모양과 맞추고, websocket 재사용이 필드마다 비교할 때 이 깃발도 들어갑니다.

출처:codex-rs/core/src/session/turn.rs 1312–1328행 · codex-rs/core/src/client.rs 946–953행

실행 쪽에는 표가 하나 더 있어요. ToolExecutorsupports_parallel_tool_calls는 기본 false입니다. 덮어쓰기를 빼먹으면 쓰기 잠금을 타요. exec_command, view_image, tool_searchtrue로 덮고, apply_patch는 안 덮어요. 라우터가 먼저 레지스트리를 찾고, 없으면 false입니다. 노출이 Hidden이면 handler가 true를 돌려도 소용없어요. MCP는 서버 스위치나 읽기 전용 힌트가 더 필요하고, 기본은 여전히 직렬입니다.

출처:codex-rs/tools/src/tool_executor.rs 122–124행 · codex-rs/core/src/tools/registry.rs 470–473행

모델은 parallel_tool_callstrue이거나 Lite에서 false인 것만 봐요. 누가 진짜 병렬인지는 못 봅니다. 어떤 도구가 실제로는 쓰기 잠금을 잡아도 도구 목록에서 항목이 빠지지는 않아요.

요청 쪽 build_prompt가 true로 씀 Lite가 AND해서 false 모델이 한 번에 여러 개를 보낼 수 있음 착지해서 겹친다는 보장은 없음 분류 표 기본 false, 병렬이려면 직접 고침 Hidden과 모르는 이름은 직렬 MCP는 opt-in 또는 읽기 전용 힌트 모델은 이 표를 못 봄 게이트 병렬은 읽기 잠금 직렬은 쓰기 잠금 RwLock 하나가 바닥 전체를 맡음 먼저 spawn, 그다음 잠금
수업용 구조도: 깃발 하나, 표 하나, 잠금 하나 — 각자 한 토막을 맡아요.
왜 오래가는가

모델에 대한 허락은 호스트 스케줄과 나눌 수 있고, 다른 언어로 다시 써도 씁니다. 요청 쪽은 한 번에 호출을 여러 개 보낼 의지만 답하고, 실행 쪽은 이번이 남과 겹칠 수 있는지만 답해요. Lite 테스트가 깃발을 끈 건, 어떤 모델 경로에서는 이 힌트를 잃는 걸 저자가 받아들인다는 뜻입니다. 게이트는 항상 있어요. 모델이 한 응답에 호출 둘을 보내도 작업 둘은 그대로 spawn하고, 로컬 표대로 잠금을 잡습니다.

아이디어 2 · 바닥 전체 읽기-쓰기 잠금이 게이트
어떤 문제를 푸는가

분류가 맞아도 문을 보는 사람은 필요해요. 읽기 둘은 같이 있을 수 있고, 쓰기 하나는 독점해야 합니다. 잠금을 먼저 잡고 MCP 서버 연결을 기다리면, 콜드 스타트 한 번이 옆에서 이미 준비된 exec_command까지 막아요.

공정성도 있어요. 뒤따라 온 읽기가 쓰기 머리 위로 넘어가면, 패치가 잠금을 영영 못 잡을 수 있어요. 표준 라이브러리 RwLock으로 바꾸면 우선순위가 OS에 매여, 쓰기 쪽이 굶을 수 있습니다.

아이디어는 무엇인가

샘플 하나가 tokio::sync::RwLock<()> 하나를 공유해요. 잠금이 지키는 값은 유닛 타입이라 업무 데이터는 없고 게이트만 합니다. 작업은 먼저 spawn하고, 선택적으로 준비를 기다린 뒤 잠금을 잡아요. 병렬이면 read, 아니면 write. 준비 대기는 잠금 밖에 있습니다. 아직 안 붙은 MCP 서버가 쓰기 잠금을 차지해 옆 shell까지 막지는 않아요.

출처:codex-rs/core/src/tools/parallel.rs 144–156행

이 잠금의 우선순위는 fair, 곧 write-preferring이에요. 줄을 선 쓰기 요청이 잡고 놓을 때까지 뒤 읽기 잠금은 나가지 않습니다. view_image가 아직 도는데 apply_patch가 이미 문 밖에 있으면, 새로 온 exec_commandview_image와 겹칠 수 있어도 패치 뒤에 서야 해요. fair가 사는 것은 쓰기 쪽이 굶지 않는 것이고, 값은 뒤따라 온 읽기가 쓰기에 가로막히는 것입니다.

분류 알갱이는 도구 인스턴스라 이번 인자는 못 봐요. exec_commandlsrm이든 읽기 잠금을 타고, apply_patch는 패치가 커도 쓰기 잠금을 탑니다. 관계없는 직렬 도구 둘도 서로 막아요. 업무 코드를 찾아도 용량 상한은 없습니다. 셸 열이 같이 들어갈 수 있고, 용량은 프로세스, 샌드박스, OS에 맡깁니다.

메뉴 · 이미 읽기 잠금을 쥠 view_image exec_command 이미 들어온 것은 계속 달림 둘이 같이 메뉴를 볼 수 있음 문 밖 · 쓰기 잠금 대기 apply_patch 읽기가 잠금을 놓기를 기다림 쓰기가 FIFO에 들어감 늦게 온 읽기 exec_command 새치기 못 함 쓰기 뒤에 섬
수업용 스케치: 이미 들어온 읽기는 계속 달리고, 뒤따라 온 읽기는 쓰기가 줄 선 걸 보고 뒤로 갑니다.
왜 오래가는가

묻는 것은 지금 이 순간에 독점자가 있느냐예요. 식당은 여럿이 메뉴를 같이 볼 수 있고, 주방에 들어가는 건 한 명만입니다. 공정 전략은 잠금 구현에 적혀 있어요. 업무 코드는 병렬인지 독점인지만 묻습니다. 새 읽기가 새치기하는 잠금으로 바꾸면 쓰기가 굶을 수 있어요. 준비 대기를 잠금 밖에 두는 것도 같은 판단입니다. 아직 준비 안 된 사람은 문간을 차지하지 말 것.

아이디어 3 · 끝난 순서가 기장 순서를 정하지 않아요
어떤 문제를 푸는가

exec_command 둘이 겹쳐 달리면, 나중에 보낸 것이 먼저 끝날 수 있어요. 끝난 순서로 이력을 쓰면 다음 라운드에 모델이 보는 결과 순서가 보낸 호출과 안 맞습니다. 스트림에서 OutputItemDone이 올 때마다 future를 만드는 쪽은 언제 착공할지를 맡아요. 여기는 착공 뒤에 누가 겹칠지, 결과를 어떤 순서로 기장할지를 맡습니다.

아이디어는 무엇인가

샘플 루프는 tool_future를 도착 순으로 FuturesOrdered에 밀어 넣어요. drain은 삽입 순으로 빼고 세션에 씁니다. 읽기 잠금이 셸 둘을 겹쳐 달리게 해도, 기장은 보낸 순서를 따릅니다. 먼저 보낸 것이 먼저 기장되고, 실제로 더 늦게 끝나도 그래요.

출처:codex-rs/core/src/session/turn.rs 2130–2140행

모델이 생각한 순서, 잠금 위에서 실제로 일어난 순서, 이력에 적힌 순서는 어긋날 수 있어요.
왜 오래가는가

관측 순서와 실행 겹침은 구조에서 갈라져요. 병렬은 벽시계만 바꾸고 장부는 안 바꿉니다. Agent를 직접 만들 때는 이 큐 둘을 적어도 따로 쓰세요. 모델에게는 병렬이라 하고, 도구를 돌릴 때는 로컬 표를 보고, 결과는 호출 목록의 원래 순서로 받습니다.

가로 비교 · 같은 문제의 다른 답

DeepSeek Harness: 인자로 분류하고, 독점이 장벽

DSH는 도구마다 isConcurrencySafe(args)를 내놓게 해요. 정확한 true만 병렬에 들어갑니다. 선언이 없거나, 인자가 잘못됐거나, 분류기가 던지면 모두 독점이에요. bash에는 분류기가 없어 통째로 독점입니다. 스케줄러는 메시지 전체가 오기를 기다려, 연속 병렬 호출을 한 묶음으로 만들고, 독점 호출마다 묶음을 따로 두어 장벽으로 씁니다. 묶음 안은 기본 상한 10인 롤링 풀이에요.

Codex는 이 묶음이 없어도 돼요. 판단을 도구 단위 불리언으로 눌러, 잠금 하나로 장벽을 흉내 내니까요. DSH는 읽기 전용 bash도 직렬로 두어 동기를 일부 포기하고, Codex는 ls 둘도, rm 둘도 겹쳐 달리게 할 수 있어요.

양쪽 모두 소스 대조 완료 · 2026-08-22

Claude Code: 인자로 나누고, 읽기 전용 bash만 병렬

Claude의 isConcurrencySafe는 기본 false예요. BashTool은 병렬을 isReadOnly에 맡기고, 명령이 읽기 전용 제약을 통과해야 true를 돌려줍니다. ls는 병렬 배치에 들어갈 수 있고, 쓰기 부작용이 있는 명령은 직렬 배치로 가요. 연속 안전 호출은 한 배치로 묶어 동기를 타고, 불안전한 것은 각각 배치가 되며 배치 안에서도 하나씩입니다. 상한은 환경 변수에서 오고, 파싱이 실패하면 10이에요.

Codex의 exec_command는 명령 파싱을 건너뛰어, 쓰기 명령도 읽기 잠금에 들어가요. 세 쪽 모두 스케줄 메타데이터를 호스트에 숨기고, 문을 닫는 자리만 다릅니다. Codex는 기본값과 Hidden에서 닫아 셸을 가장 풀어 주고, DSH는 분류기에서 닫아 bash를 전부 직렬로 두며, Claude는 가운데에 있어요.

양쪽 모두 소스 대조 완료 · 2026-08-22
수업 실습
01

늦게 온 읽기가 새치기할 수 있나

view_image가 아직 도는데 apply_patch는 이미 문 밖에 줄을 섰어요. 그때 모델이 exec_command를 하나 더 보냅니다. 데모에서 늦게 온 읽기로 바꾸고 단스텝으로 끝까지 간 뒤, 아래 세 질문에 맞춰 보세요.

exec_command가 아직 도는 view_image와 겹쳐 달릴 수 있나요.
결과 셋이 이력에 어떤 순서로 기장되나요.
apply_patch 칸을 읽기 패로 바꾸면, 게이트가 그걸 따로 막을까요.
Takeaway:모델에게 병렬이라 하는 것은 요청 쪽 깃발이에요. 누가 겹쳐 달릴지는 도구가 기본값을 고쳤는지에 달리고, Hidden과 모르는 이름은 독점입니다. 끝난 뒤 이력은 보낸 순서로 기장해요. 순서 셋은 어긋날 수 있습니다.