13 작업

먼저 알아야 할 것: 4장(블록), 6장(큐), 8장(결함).

이 장이 답하는 물음

13.1 번갈아 돌기

작업은 한 상태 안의 실행 흐름이다. 스레드가 아니다 — 작업은 스스로 yield 하는 자리에서만 바뀐다. 그래서 두 yield 사이에서는 다른 작업이 끼어들지 않고, 자물쇠가 필요 없다.

tasks 이름 수 가 작업 묶음을 선언하고, 블록 묶음 spawn 이 작업을 만들고, 묶음 run_all 이 모두 끝날 때까지 돌린다.

13-turns.pnx

use io
tasks workers 2
word count_to (name str, n int)
  1 to i
  loop
    i n gt until
    cat (name, " ", i) io.println
    yield or_return                         rem give the other task a turn
    i 1 add to i
  end
end
word demo ()
  do "ping" 3 count_to end workers spawn or_else (e) return end drop   rem spawn gives the task; not needed here
  do "pong" 2 count_to end workers spawn or_else (e) return end drop   rem nothing runs yet: both wait for a turn
  workers run_all                                                      rem now they run, until both are done
end
demo

출력

$ pnx 13-turns.pnx
ping 1
pong 1
ping 2
pong 2
ping 3

spawn 한 줄을 따라가면 작업이 무엇을 받고 무엇을 주는지 보인다.

토큰그 뒤의 스택일어난 일
do "ping" 3 count_to end블록받는 것도 주는 것도 없는 블록이 값으로 올라간다. 안의 리터럴은 블록이 든다.
workers spawn작업묶음에 작업 하나를 만들고 그 작업을 준다. 자리가 없으면 실패한다.
or_else (e) return end작업성공: 건너뛴다. 실패였다면 낱말을 떠난다.
drop—작업 값은 resume 이나 cancel 에 쓰지만 여기서는 run_all 이 돌리므로 버린다.

spawn 은 작업을 만들기만 하고 돌리지 않는다. run_all 이 묶음의 작업에 차례로 한 번씩 돌 기회를 주고, 작업은 yield 에서 차례를 내놓는다. 출력이 번갈아 나오는 까닭을 차례마다 적으면 이렇다.

차례도는 작업출력그다음
1pingping 1yield: 차례를 내놓는다
2pongpong 1yield
3pingping 2yield
4pongpong 2yield
5pingping 3yield
6pong(없음)i 가 3 이 되어 until 로 떠난다. 작업이 끝난다
7ping(없음)i 가 4. 작업이 끝난다. 모두 끝났으므로 run_all 이 돌아온다

각 작업은 자기 지역 변수(name, n, i)와 자기 스택을 따로 갖는다. 그래서 yield 로 갔다가 돌아와도 하던 자리 그대로 이어서 돈다.

13.2 큐로 잇기

작업 사이의 자료는 선언된 저장소로 주고받는다. 큐가 가장 흔하다. 큐 wait_take 는 큐에서 꺼내고 v 큐 wait_put 은 넣는다. 큐가 비어(차) 있는 동안 작업은 양보하고, 다음 차례에 다시 본다.

13-pipeline.pnx

use io
rem Three stages linked by queues: numbers -> squares -> total.
tasks stages 3
queue numbers int 2
queue squares int 2
global total int
word produce ()
  1 to i
  loop
    i 5 gt until
    i numbers wait_put or_return              rem waits while the queue is full
    i 1 add to i
  end
  -1 numbers wait_put or_return
end
word square ()
  loop
    numbers wait_take or_return to n          rem waits while the queue is empty
    n 0 lt if -1 squares wait_put or_return return end
    n n mul squares wait_put or_return
  end
end
word add_up ()
  loop
    squares wait_take or_return to n
    n 0 lt until
    total n add set total
  end
end
word demo ()
  do produce end stages spawn or_else (e) return end drop
  do square end stages spawn or_else (e) return end drop
  do add_up end stages spawn or_else (e) return end drop
  stages run_all
  cat ("1 + 4 + 9 + 16 + 25 = ", total) io.println
end
demo

출력

$ pnx 13-pipeline.pnx
1 + 4 + 9 + 16 + 25 = 55

세 단계가 각각 작업이고, 큐의 용량은 둘뿐이다. 단계는 서로를 기다리며 번갈아 돈다. 끝을 알리는 데는 -1 을 보냈다.

모든 작업이 아무도 채우지 않을 큐를 기다리면 더 갈 수 없다. run_all 은 이것을 알아보고, 끝없이 도는 대신 결함 deadlock 으로 실행을 끝낸다. 작업이 아닌 주 실행에서 큐를 기다리는 것은 바로 그 결함이다.

13-deadlock.pnx

use io
tasks stages 2
queue numbers int 2
queue squares int 2
word square ()
  numbers wait_take or_return to n
  n n mul squares wait_put or_return
end
word show ()
  squares wait_take or_return to n
  cat ("square: ", n) io.println
end
word demo ()
  do square end stages spawn or_else (e) return end drop
  do show end stages spawn or_else (e) return end drop
  stages run_all                              rem no one puts into numbers
end
demo

결함으로 멈춤

$ pnx 13-deadlock.pnx
13-deadlock.pnx:16: deadlock: waiting for a queue that nothing can change

지켜보는 것은 두 기다림 낱말뿐이다. 스스로 도는 대기(loop … yield … end)는 보지 않으므로, 그런 코드를 돌리는 호스트는 예산을 준다(14장).

흔한 오해: spawn 의 블록은 바깥의 지역 변수를 본다

spawn 에 주는 블록은 값이 되는 블록이라 바깥의 지역 변수를 보지 못한다. 작업은 그 낱말이 끝난 뒤에도 살 수 있기 때문이다. 작업에 값을 넘기려면 전역이나 큐를 쓰거나, 위의 예처럼 블록 안에서 낱말을 부르며 리터럴을 준다.

13-no-locals.pnx

tasks workers 2
word start (n int) do n drop end workers spawn or_else (e) return end drop end

거절

$ pnx 13-no-locals.pnx
13-no-locals.pnx:2:23: this name is not declared before this place

13.3 결함과 취소

작업 하나의 결함은 그 실행 전체를 끝낸다 — 다른 작업도 함께 멈춘다. 상태는 남아서 다음 실행을 받는다.

13-fault.pnx

use io
tasks workers 2
global zero int
word demo ()
  do "the healthy task starts" io.println yield or_return "and never finishes" io.println end workers spawn or_else (e) return end drop
  do 1 zero div drop end workers spawn or_else (e) return end drop
  workers run_all
end
demo

결함으로 멈춤

$ pnx 13-fault.pnx
the healthy task starts
13-fault.pnx:6: division by zero

작업들이 하는 일이 다른 작업까지 끌고 내려가서는 안 될 때(서버의 요청, 큐에서 받은 일)에는 묶음을 apart 로 선언한다. 그러면 결함은 그것이 난 작업만 끝내고, 다른 작업과 실행은 계속 간다.

13-apart.pnx

use io
tasks workers 2 apart
global zero int
word demo ()
  do "the healthy task starts" io.println yield or_return "and finishes" io.println end workers spawn or_else (e) return end drop
  do 1 zero div drop end workers spawn or_else (e) return end drop
  workers run_all
  "the run goes on" io.println
end
demo

출력

$ pnx 13-apart.pnx
the healthy task starts
and finishes
the run goes on

작업 cancel 은 그 작업의 다음 yield 나 큐 기다림을 cancelled 로 실패시킨다. 작업은 그 실패를 or_else 로 받아 정리하고 끝낸다. 취소는 작업을 억지로 멈추지 않는다.

문답

물음. 작업은 몇 개까지 만들 수 있는가?

답. tasks 이름 수 의 수 까지다(최대 4096). 끝난 작업의 자리는 다시 쓰인다. 작업마다 자기 호출 깊이와 구역이 있으니 수가 크면 덩이도 커야 한다.

문답

물음. 주 실행에서 yield 를 부르면?

답. 아무 일도 없이 성공한다. 주 실행은 작업이 아니라 차례를 넘길 데가 없다. 그래서 같은 낱말을 작업 안에서도 밖에서도 쓸 수 있다.

정리