14 작업
구문
tasks-decl = "tasks" NAME size [ size [ size [ size ] ] ] [ "apart" ] ; (* count, call depth, region bytes, turn limit *)(1) 작업은 한 상태 안에서 번갈아 도는 실행 흐름이다. 작업은 스스로 양보하는 자리(yield, 기다리는 호스트 낱말)에서만 바뀐다. 그래서 두 작업이 같은 전역을 쓰더라도, 한 작업의 두 양보 사이에서는 다른 작업이 끼어들지 않는다. 작업은 스레드가 아니다 — 한 상태는 한 번에 한 스레드에서만 돈다(5.1).
(2) tasks 이름 수 [깊이 [구역 [차례]]] 은 작업 묶음(task group)을 선언한다. 묶음은 작업을 수 개까지 담는다. 작업마다 자기 호출 깊이(깊이)와 자기 구역(구역 바이트)이 있다. 줄이면 처리기의 기본값이다. 차례 를 쓰면 그것이 묶음의 차례 한도다(14.6). 묶음의 이름은 그 묶음의 작업을 가리키는 값의 타입이다. 끝에 apart 를 쓰면 묶음의 작업 하나에서 난 결함이 그 작업만 끝낸다(14.4 (3a)). apart 는 그 자리에서만 선언의 낱말이다.
14.1 만들기와 돌리기
(1) 블록 묶음 spawn 은 받는 것도 주는 것도 없는 값이 되는 블록(10.5)으로 작업 하나를 만들고 그 작업을 준다(or fail, 빈 자리가 없으면 err.full). 작업은 만들어진 채 멈춰 있고, 아직 돌지 않는다.
(2) 묶음 run_all 은 그 묶음의 작업을 차례로, 모두 끝날 때까지 돌린다. 한 작업은 양보하거나 끝날 때까지 돌고, 그다음 작업이 이어 돈다.
14-tasks.pnx
use io
tasks workers 3
word say (s str) s io.println end
word demo ()
do "a1" say yield or_return "a2" say yield or_return "a3" say end workers spawn or_else return end drop
do "b1" say yield or_return "b2" say end workers spawn or_else return end drop
workers run_all rem run every task of the group until all have ended
end
demo
출력
$ pnx 14-tasks.pnx
a1
b1
a2
b2
a3
(3) 작업 resume 은 그 작업 하나를 다음 양보나 끝까지 돌리고, 끝났으면 true 를 준다. 작업 is_done 은 끝났는지 묻는다. 끝난 작업의 자리는 다시 spawn 에 쓰인다.
14-resume.pnx
use io
use str
tasks lone 1
word demo ()
do "step 1" io.println yield or_return "step 2" io.println end lone spawn or_else return end to t
0 to turns
loop t resume until turns 1 add to turns end rem resume gives true when the task has ended
cat ("resumed ", turns 1 add, " times, done: ", t is_done) io.println
end
demo
출력
$ pnx 14-resume.pnx
step 1
step 2
resumed 2 times, done: true
(4) 작업은 상태 안에 남는다. 호스트가 부른 한 실행이 끝나도 멈춘 작업은 그대로 있고, 다음 실행에서 이어 돌릴 수 있다.
14-full.pnx
use io
tasks pair 2
word demo ()
do yield or_return end pair spawn or_else return end drop
do yield or_return end pair spawn or_else return end drop
do end pair spawn or_else (e) cat ("third spawn: ", e err_text) io.println return end drop
end
demo
출력
$ pnx 14-full.pnx
third spawn: full
14.2 양보
(1) yield 는 다른 작업에 차례를 넘긴다. 실패할 수 있다(or fail): 작업이 취소되었으면 err.cancelled 로 실패한다. 작업이 아닌 주 실행에서 yield 는 아무것도 하지 않고 바로 성공한다.
(2) 작업 사이의 자료는 전역, 큐, 목록으로 나눈다. 가득 찬 큐나 빈 큐는 양보할 까닭이 된다.
14-queue.pnx
use io
use str
tasks workers 2
queue jobs int 2
list done int 8
word producer ()
1 to i
loop
i 5 gt until
true to ok
i jobs put or_else (e) false to ok end
ok if i 1 add to i else yield or_return end rem a full queue: let the consumer run
end
end
word consumer ()
0 to got
loop
got 5 ge until
jobs take or_else (e) -1 end to v
v 0 lt if yield or_return else v done push or_else (e) end got 1 add to got end
end
end
word demo ()
do producer end workers spawn or_else return end drop
do consumer end workers spawn or_else return end drop
workers run_all
end
demo
done do (v int) v str.to_str io.print " " io.print end each
"" io.println
출력
$ pnx 14-queue.pnx
1 2 3 4 5
(3) yield 를 (직접이든, 부른 낱말 안에서든) 할 수 있는 낱말은 적재 때 알려진다. wait_take 와 wait_put(14.3)도 양보한다. 호스트 낱말 안에서 그런 낱말은 돌리지 못한다(19절).
14.3 큐 기다리기
(1) 큐 wait_take 는 take 처럼 큐의 맨 앞 값을 꺼내되, 큐가 비어 있는 동안은 양보하고 차례가 돌아오면 다시 본다. v 큐 wait_put 은 put 처럼 넣되, 큐가 차 있는 동안 양보한다. 둘 다 실패할 수 있다(or fail): 기다리는 동안 취소된 작업은 err.cancelled 로 실패한다. 값을 바로 꺼내거나 넣을 수 있을 때는 취소를 보지 않는다.
14-queue-wait.pnx
use io
use str
tasks workers 2
queue jobs int 1
list done int 8
word producer ()
1 to i
loop
i 5 gt until
i jobs wait_put or_return rem waits while the queue is full
i 1 add to i
end
-1 jobs wait_put or_return
end
word consumer ()
loop
jobs wait_take or_return to v rem waits while the queue is empty
v 0 lt until
v done push or_else (e) end
end
end
word demo ()
do producer end workers spawn or_else (e) return end drop
do consumer end workers spawn or_else (e) return end drop
workers run_all
end
demo
done do (v int) v str.to_str io.println end each
출력
$ pnx 14-queue-wait.pnx
1
2
3
4
5
(2) 큐를 기다리는 작업은 어떤 큐든 바뀌기 전에는 다시 돌지 않는다. run_all 이 남은 작업이 모두 큐를 기다리고 호스트를 기다리는 작업은 없다는 것을 알면, 이제 큐를 바꿀 것이 없다: 이것이 결함 deadlock 이다. apart 없는 묶음의 작업 안의 결함처럼(14.4) 모든 작업을 끝낸다. apart 묶음의 작업도 끝낸다. 호스트를 기다리는 작업이 있으면 상태는 전처럼 기다리고(PINAX_WAITING), 그 작업이 이어 돌며 하는 일이 다른 작업을 풀 수 있다.
14-deadlock.pnx
use io
tasks workers 2
queue asks int 1
queue answers int 1
word demo ()
do asks wait_take or_return drop 1 answers wait_put or_return end workers spawn or_else (e) return end drop
do answers wait_take or_return drop 1 asks wait_put or_return end workers spawn or_else (e) return end drop
workers run_all rem each waits for the other: nothing can go
end
demo
"not reached" io.println
결함으로 멈춤
$ pnx 14-deadlock.pnx
14-deadlock.pnx:8: deadlock: waiting for a queue that nothing can change
(3) 주 실행은 작업이 아니어서 차례를 넘길 데가 없다. 주 실행에서 빈 큐의 wait_take, 찬 큐의 wait_put 은 바로 결함 deadlock 이다.
14-deadlock-main.pnx
use io
use str
queue jobs int 2
word demo ()
jobs wait_take or_else (e) -1 end str.to_str io.println rem the main run is not a task: no one could fill the queue
end
demo
결함으로 멈춤
$ pnx 14-deadlock-main.pnx
14-deadlock-main.pnx:5: deadlock: waiting for a queue that nothing can change
(4) 교착을 보는 것은 이 두 낱말뿐이다. 스스로 도는 대기(loop … yield … end)나 resume 으로 손수 돌리는 작업은 보지 않는다. 그런 코드를 돌리는 호스트는 예산을 준다(19.5).
14.4 취소
(1) 작업 cancel 은 작업에 취소를 알린다. 그 작업의 다음 yield 나 큐 기다림(14.3)이 err.cancelled 로 실패한다. 작업은 그 실패를 받아 자기 방식으로 끝난다 — 취소는 작업을 억지로 멈추지 않는다.
14-cancel.pnx
use io
tasks workers 1
word demo ()
do
loop
"working" io.println
yield or_else (e) cat ("stopped: ", e err_text) io.println return end
end
end workers spawn or_else return end to t
t resume drop
t resume drop
t cancel rem the task's next yield fails with cancelled
t resume drop
end
demo
출력
$ pnx 14-cancel.pnx
working
working
stopped: cancelled
제약
(2) spawn 에 주는 블록은 값이 되는 블록이라 바깥의 지역 변수를 보지 못한다. 작업에 값을 주려면 전역이나 큐를 쓴다.
14-spawn-refused.pnx
tasks workers 2
word demo (n int) do n drop end workers spawn or_else return end drop end
거절
$ pnx 14-spawn-refused.pnx
14-spawn-refused.pnx:2:22: this name is not declared before this place
(2a) spawn 의 블록은 받는 것도 주는 것도 없어야 한다. 매개변수가 있는 블록은 거절된다.
14-spawn-block.pnx
tasks workers 2
word start () do (n int) n drop end workers spawn or_else return end drop end
거절
$ pnx 14-spawn-block.pnx
14-spawn-block.pnx:2:45: spawn takes a block that takes and gives nothing, and a set of tasks: do ... end workers spawn
결함
(3) apart 없는 묶음의 작업 안의 결함은 그 실행 전체와 모든 작업을 끝낸다. 결함은 주 실행의 결함처럼 호스트에게 간다. 상태는 남아 다음 실행을 받는다.
14-fault.pnx
use io
tasks workers 2
global zero int
word demo ()
do "task one" io.println yield or_return "task one again" io.println end workers spawn or_else return end drop
do 1 zero div drop end workers spawn or_else return end drop
workers run_all
end
demo
"not reached" io.println
결함으로 멈춤
$ pnx 14-fault.pnx
task one
14-fault.pnx:6: division by zero
(3a) apart 로 선언한 묶음의 작업 안의 결함은 그 작업만 끝낸다. 그 자리는 다시 비고, 그 작업을 이어 돌린 쪽은 작업이 끝났을 때처럼 이어 간다. run_all 은 다음 작업을 잡고, resume 은 true 를 준다. 작업이 그때까지 저장한 것은 남는다. 실행은 계속되고, 호스트는 pinax_task_faults 로 그 결함을 안다(19.10 (3)).
14-apart.pnx
use io
tasks workers 3 16 1024 apart
word work (n int) cat ("100 / ", n, " = ", 100 n div) io.println end
word demo ()
do 4 work end workers spawn or_else (e) return end drop
do 0 work end workers spawn or_else (e) return end drop rem this one divides by zero
do 5 work end workers spawn or_else (e) return end drop
workers run_all
"the others finished" io.println
end
demo
출력
$ pnx 14-apart.pnx
100 / 4 = 25
100 / 5 = 20
the others finished
(4) 두 작업이 같은 구역을 쓰는 일은 12.3 의 검사가 보지 않는다. 작업마다 자기 구역이 있으므로 보통은 일어나지 않는다.
진단
| 조건 | 메시지 |
|---|---|
매개변수나 결과가 있는 블록을 spawn 에 줌 | spawn takes a block that takes and gives nothing, and a set of tasks: do ... end workers spawn |
작업이 아닌 값에 resume is_done cancel | this word takes a task |
작업 묶음이 아닌 모음(배열, 목록)에 run_all | run_all takes a set of tasks |
tasks 의 수가 4096 을 넘음 | a set holds at most 4096 tasks |
| 작업의 호출 깊이가 1 부터 4096 밖 | the call depth of a task is 1 to 4096 |
| 작업의 구역이 16 MiB 를 넘음 | the region of a task is at most 16 MiB |
자료 소스의 tasks | a data source runs no code and declares no tasks |
int 가 아닌 값에 task_tag | task_tag takes an int |
| 1 보다 작은 차례 한도 | the turn limit of a task is at least 1 |
14.5 작업의 이름
(1) n task_tag(int gives)는 도는 작업에 꼬리표(tag)를 준다. 꼬리표는 그 묶음이 apart 로 선언되었을 때 작업의 결함과 함께 호스트에게 알려지는 int 다(14.4 (3a), 19.10 (3)). 작업의 꼬리표는 작업이 주기 전까지 0 이고, 나중의 task_tag 가 앞의 것을 바꾼다. 작업이 아닌 주 실행에서 이 낱말은 아무것도 하지 않는다. 꼬리표는 작업의 일부이고, 상은 그것을 지닌다.
(2) 작업은 자기가 맡을 일을 spawn 뒤에 큐나 전역에서 받으므로(14.4 (2)), 이름도 그때 정한다. 요청마다 번호를 주는 호스트는 작업에 그 번호를 건네고, 작업은 그것을 꼬리표로 준다.
14-tag.pnx
use io
tasks workers 2 16 1024 apart
queue jobs int 4
word work ()
jobs wait_take or_return to n
n task_tag rem the host is told this tag if the task faults
cat ("job ", n, ": ", 100 n div) io.println
end
word demo ()
4 jobs wait_put or_return 5 jobs wait_put or_return
do work end workers spawn or_else (e) return end drop
do work end workers spawn or_else (e) return end drop
workers run_all
end
demo
출력
$ pnx 14-tag.pnx
job 4: 25
job 5: 20
14.6 차례 한도
(1) 작업의 차례(turn)는 작업이 실행을 얻을 때(run_all 이나 resume 에서)부터 양보하거나 기다리거나 끝날 때까지다. 차례 한도와 함께 선언한 묶음(14 (2))은 그 작업의 차례마다 명령을 많아야 그만큼 허락한다. 세는 법은 예산과 같다(19.5). 뒤로 가는 분기와 호출의 검사마다 그 비용을 세고, 훑은 바이트도 센다. 차례마다 처음부터 센다.
(2) 차례 한도를 넘은 작업은 그 자리에서 cancelled 로 결함을 낸다. 거기에는 실패를 받도록 적힌 것이 없으므로, 작업은 이것을 실패로 받을 수 없다. apart 로 선언한 묶음에서는 그 작업만 끝나고(14.4 (3a)), 호스트는 그런 결함이 다 그렇듯 이것을 듣는다(19.10 (3a)). apart 없는 묶음에서는 작업 안의 다른 결함처럼 실행을 끝낸다(14.4 (3)). 그래서 양보하지 않고 도는 작업을, 다른 작업을 멈추지 않고 멈출 수 있다.
14-turns.pnx
use io
tasks workers 2 16 1024 5000 apart rem a turn: at most 5000 before the task yields, waits or ends
word stuck () 0 to i loop i 1 add to i end end rem never yields
word steady ()
0 to i
loop i 1 add to i i 100000 eq until i 100 mod 0 eq if yield or_return end end
cat ("steady counted to ", i) io.println
end
word demo ()
do stuck end workers spawn or_else (e) return end drop
do steady end workers spawn or_else (e) return end drop
workers run_all
"the stuck task was stopped; the steady one finished" io.println
end
demo
출력
$ pnx 14-turns.pnx
steady counted to 100000
the stuck task was stopped; the steady one finished
(3) 한도와 예산은 따로 센다. 차례 안에서 예산이 다하면 실행이 멈추고, 호스트가 실행을 이어 가면 그 차례는 그때 남았던 만큼으로 이어 간다. 그때 예산에 남아 있던 것, 곧 예산이 다한 검사의 비용보다 작은 것은 차례에 쓴 것으로 센다. 다른 작업이 이어 돌린 작업은 이어 돌린 작업의 차례 위에서 돌고, 그 차례에도 센다.