Threads

Threads

스레드(thread)는 동시 실행의 기본 단위예요. The Racket Guide의 Concurrency and Synchronization에서 스레드를 소개하고 있어요. Racket 스레드 모델에 대한 기본 정보는 Threads 문서를, 관련 내용은 Futures와 Places 문서도 참고하세요.

스레드가 만들어지면 현재 커스토디언(custodian)의 관리 아래 들어가고 현재 스레드 그룹에 추가돼요. 스레드는 thread-resume을 통해 추가되는 커스토디언 매니저를 얼마든지 가질 수 있어요. 스레드가 만든 할당은 그 스레드의 커스토디언 매니저에게 귀속돼요. 예시는 custodian-limit-memory를 참고하세요.

종료되지 않은 스레드는, 도달 불가능하고 일시중단(suspended)되었거나, 도달 불가능하고 semaphore-wait, semaphore-wait/enable-break, channel-put, channel-get, sync, sync/enable-break, thread-wait 같은 함수를 통해 오직 도달 불가능한 이벤트들에만 블록되어 있다면 가비지 컬렉션(Garbage Collection 문서 참고)될 수 있어요. 단, place-channel 블로킹에 대한 제한을 조심하세요. Places 문서의 주의사항을 참고하세요.

GRacket에서 이벤트스페이스(eventspace)의 핸들러 스레드는 이벤트 큐가 비어 있을 때 내부 세마포어에 블록돼요. 따라서 그 이벤트스페이스가 도달 불가능하고 보이는 창이나 실행 중인 타이머가 없으면 핸들러 스레드는 수집 가능해요.

스레드는 동기화 가능한 이벤트(Events 문서 참고)로 사용될 수 있어요. thread-wait가 블록되지 않을 때 스레드는 동기화 준비가 되고, 스레드의 동기화 결과는 스레드 자신이에요.

출처: Racket Reference

본문

Creating Threads

(thread thunk [#:pool pool #:keep keep]) → thread?
thunk : (-> any)
pool  : (or/c #f 'own parallel-thread-pool?) = #f
keep  : (or/c #f 'results) = #f

procedure

새 제어 스레드에서 인자 없이 thunk를 호출해요. thread 프로시저는 스레드 디스크립터 값과 함께 즉시 돌아와요. thunk의 호출이 돌아오면 thunk를 호출하기 위해 만들어진 스레드는 종료돼요.

pool#f이면 결과 스레드는 코루틴 스레드(coroutine thread)예요. pool'own이면 새 병렬 스레드 풀(parallel thread pool)이 만들어지고, 스레드가 그 풀에 추가되며, 풀은 추가 스레드에 대해 (parallel-thread-pool-close의 의미에서) 닫혀요. pool이 병렬 스레드 풀이면 새 스레드가 그 풀에서 만들어지며, 이는 같은 풀의 다른 스레드들과 프로세서 자원을 공유한다는 뜻이에요. 병렬 스레드와 성능에 대한 정보는 Parallel Threads 문서를 참고하세요. 병렬 스레드는 BC 변형의 Racket이나 병렬성 지원 없이 빌드된 Racket에서는 병렬로 실행되지 않아요. 자세한 내용은 병렬 스레드 풀에 대한 설명을 참고하세요.

keep'results이면 thread-wait가 보고할 수 있도록 결과가 그 스레드와 함께 기록돼요. 그렇지 않으면 thunk의 결과는 무시돼요.

base 패키지 8.18.0.2 버전에서 변경됨: #:pool#:keep 인자가 추가되었어요.

(thread? v) → thread?
v : any/c

procedure

v가 스레드 디스크립터이면 #t, 그렇지 않으면 #f를 돌려줘요.

(current-thread) → thread?

procedure

현재 실행 중인 스레드의 스레드 디스크립터를 돌려줘요.

(thread/suspend-to-kill thunk) → thread?
thunk : (-> any)

procedure

thread와 같지만, kill-threadcustodian-shutdown-all로 스레드를 "죽이는" 것이 스레드를 종료하는 대신 단지 일시중단시킬 뿐이에요.

(call-in-nested-thread thunk [cust]) → any
thunk : (-> any)
cust  : custodian? = (current-custodian)

procedure

thunk을 실행하기 위해 cust가 관리하는 중첩 스레드를 만들어요. (중첩 스레드의 현재 커스토디언은 cust 인자와 무관하게 만든 스레드에서 상속돼요.) 현재 스레드는 thunk이 돌아올 때까지 블록되고, call-in-nested-thread 호출의 결과는 thunk이 돌려준 결과예요.

중첩 스레드의 예외 핸들러는 스레드의 시작 부분으로 점프하고 예외를 원래 스레드로 전달하는 프로시저로 초기화돼요. 따라서 그 핸들러는 중첩 스레드를 종료하고 원래 스레드에서 예외를 다시 일으켜요.

call-in-nested-thread가 만든 스레드가 thunk이 돌아오기 전에 죽으면 원래 스레드에서 exn:fail 예외가 일어나요. 원래 스레드가 thunk이 돌아오기 전에 죽으면 중첩 스레드를 위해 브레이크(break)가 큐에 들어가요.

중첩 스레드가 실행되는 동안 원래 스레드에 브레이크가 큐에 들어가면(break-thread로), 그 브레이크는 중첩 스레드로 리다이렉트돼요. 중첩 스레드가 만들어질 때 원래 스레드에 이미 브레이크가 큐에 있으면 그 브레이크는 중첩 스레드로 옮겨져요. 중첩 스레드가 완료될 때 브레이크가 여전히 큐에 남아 있으면 그 브레이크는 원래 스레드로 옮겨져요.

call-in-nested-thread가 만든 스레드가 자기 자신의 call-in-nested-thread 호출 중에 죽으면, 바깥쪽 call-in-nested-thread 호출은 가장 안쪽 중첩 스레드가 완료될 때까지 기다리고, 안쪽 스레드에 보류 중인 브레이크는 원래 스레드로 옮겨져요.

Suspending, Resuming, and Killing Threads

(thread-suspend thd) → void?
thd : thread?

procedure

thd가 실행 중이면 그 실행을 즉시 일시중단해요. 스레드가 종료되었거나 이미 일시중단되어 있으면 thread-suspend는 효과가 없어요. 스레드는 thread-resume으로 재개될 때까지 일시중단 상태로 남아요 (즉 실행하지 않아요). 현재 커스토디언이 thd를 단독으로 관리하지 않으면(즉 thd의 어떤 커스토디언이 현재 커스토디언이나 그 하위가 아니면), exn:fail:contract 예외가 일어나고 스레드는 일시중단되지 않아요.

(thread-resume thd [benefactor]) → void?
thd        : thread?
benefactor : (or/c thread? custodian? #f) = #f

procedure

thd가 일시중단되어 있고 커스토디언이 하나 이상 있으면(아래 설명처럼 benefactor를 통해 추가될 수 있음) 그 실행을 재개해요. 스레드가 종료되었거나, 스레드가 이미 실행 중인데 benefactor가 주어지지 않았거나, 스레드에 커스토디언이 없는데 benefactor가 주어지지 않았다면 thread-resume은 효과가 없어요. 그렇지 않으면, benefactor가 주어지면 최대 세 가지 추가 동작이 트리거돼요:

  • benefactor가 스레드이면, 미래에 그것이 일시중단 상태에서 재개될 때마다 thd도 재개돼요. (thd를 재개하면 이전에 thread-resume을 통해 thd에 붙었던 다른 스레드들의 재개가 트리거될 수 있어요.)
  • thd의 매니저 집합에 새 커스토디언이 추가될 수 있어요. benefactor가 스레드이면 그 스레드의 모든 커스토디언이 thd에 추가돼요. 그렇지 않으면 benefactor는 커스토디언이고, (이미 종료되지 않았다면) 그것이 thd에 추가돼요. thd가 커스토디언과 그 하위 중 하나 이상 둘 다에게 관리되게 되면 중복된 하위들이 thd에서 제거돼요. thd가 일시중단되어 있고 커스토디언이 추가되면 thd는 추가된 후에만 재개돼요.
  • benefactor가 스레드이면, 미래에 그것이 새 관리 커스토디언을 받을 때마다 thd도 그 커스토디언을 받아요. (thd에 커스토디언을 추가하면 이전에 thread-resume을 통해 thd에 붙었던 다른 스레드들에 커스토디언 추가를 트리거할 수 있어요.)
(kill-thread thd) → void?
thd : thread?

procedure

지정된 스레드를 즉시 종료하거나, thdthread/suspend-to-kill로 만들어졌으면 그 스레드를 일시중단해요. 메인 스레드를 종료하면 애플리케이션이 종료돼요. thd가 이미 종료되었으면 kill-thread는 아무것도 하지 않아요. 현재 커스토디언이 thd를 단독으로 관리하지 않으면(즉 thd의 어떤 커스토디언이 현재 커스토디언이나 그 하위가 아니면), exn:fail:contract 예외가 일어나고 스레드는 죽지 않거나 일시중단되지 않아요.

달리 언급되지 않는 한 Racket(및 GRacket)이 제공하는 프로시저는 kill-safe이고 suspend-safe예요. 즉 스레드를 죽이거나 일시중단해도 다른 스레드에서 프로시저를 적용하는 것을 절대 방해하지 않아요. 예를 들어 입력 포트에서 문자를 추출하는 동안 스레드가 죽으면 그 문자는 완전히 소비되거나 소비되지 않으며, 다른 스레드가 그 포트를 안전하게 사용할 수 있어요.

(break-thread thd [kind]) → void?
thd  : thread?
kind : (or/c #f 'hang-up 'terminate) = #f

procedure

지정된 스레드에 브레이크를 등록해요. 선택적 kind 값은 등록할 브레이크의 종류를 나타내며, #f, 'hang-up, 'terminate는 각각 인터럽트, hang-up, terminate 브레이크에 대응해요. thd에서 브레이킹이 비활성화되어 있으면 브레이크가 다시 활성화될 때까지 무시돼요. 자세한 내용은 Breaks 문서를 참고하세요.

(sleep [secs]) → void?
secs : (>=/c 0) = 0

procedure

현재 스레드가 잠든 후 최소 secs 초가 지날 때까지 잠들게 해요. secs의 0 값은 단순히 다른 스레드가 실행되도록 허용하는 힌트로 작용해요. secs 값은 비정수일 수 있어 임의의 정밀도로 잠자기 시간을 요청할 수 있어요. 실제 잠자기 시간의 정밀도는 지정되지 않아요.

(thread-running? thd) → any
thd : thread?

procedure

thd가 종료되지 않았고 일시중단되지 않았으면 #t, 그렇지 않으면 #f를 돌려줘요.

(thread-dead? thd) → any
thd : thread?

procedure

thd가 종료되었으면 #t, 그렇지 않으면 #f를 돌려줘요.

Synchronizing Thread State

(thread-wait thd [fail-k]) → any
thd    : thread?
fail-k : (procedure-arity-includes/c 0) = void

procedure

thd가 종료될 때까지 현재 스레드의 실행을 블록해요. 스레드의 프로시저가 예외를 일으켰거나, 달리 스레드의 초기 프롬프트로 중단(abort)되었거나, 죽임으로써 종료되었다면 fail-k가 호출되어 thread-wait의 결과를 만들어요. 그렇지 않으면, 스레드가 결과를 기록하면(thread#:keep 참고) 그 결과들이 돌려지고, 스레드가 결과를 유지하지 않으면 #<void>가 돌려져요.

(thread-wait (current-thread))는 현재 스레드를 교착(deadlock)상태로 만든다는 점에 주의하세요. 하지만 브레이크가 활성화되어 있고 그 스레드가 메인 스레드이거나 달리 접근 가능하면 브레이크가 교착을 끝낼 수 있어요. Breaks 문서를 참고하세요.

thdthread/suspend-to-kill로 만들어지지 않은 한, (thread-wait thd)thd가 달리 접근 불가능하더라도 잠재적으로 계속될 수 있는데, 커스토디언 종료가 그 스레드를 종료할 수 있기 때문이에요. 결과적으로 thread-wait로 블록된 스레드는 일반적으로 가비지 컬렉션될 수 없어요 (Garbage Collection 문서 참고). 그러나 특수한 경우로, thd가 현재 스레드이면 (thread-wait thd)는 스레드의 가비지 컬렉션을 막지 않고 블록돼요. 그 스레드는 브레이크가 대기에서 빠져나갈 때만 계속될 수 있기 때문이에요.

base 패키지 8.18.0.2 버전에서 변경됨: 값이 있는 스레드와 fail-k 인자에 대한 지원이 추가되었어요.

(thread-dead-evt thd) → evt?
thd : thread?

procedure

thd가 종료된 경우에만 동기화 준비가 되는 동기화 가능한 이벤트(Events 문서 참고)를 돌려줘요. 하지만 thd를 직접 사용하는 것과 달리, 이벤트에 대한 참조를 유지해도 thd가 가비지 컬렉션되는 것을 막지 않아요 (Garbage Collection 문서 참고). thread-dead 이벤트의 동기화 결과는 thread-dead 이벤트 자신이에요.

(thread-dead-evt thd)의 결과를 기다리는 스레드는 일반적으로 자기 자신이 가비지 컬렉션될 수 없어요. 단, thdthread/suspend-to-kill로 만들어졌다면 thread-wait로 기다리는 것과 같은 방식으로 가능해요. 그러나 thd가 현재 스레드인 (thread-dead-evt thd)의 결과를 기다리는 것에 대한 특수한 경우는 없어요.

주어진 thd에 대해 thread-dead-evt는 항상 같은(즉 eq?한) 결과를 돌려줘요.

(thread-resume-evt thd) → evt?
thd : thread?

procedure

thd가 실행 중일 때 동기화 준비가 되는 동기화 가능한 이벤트(Events 문서 참고)를 돌려줘요. (thd가 종료되었으면 이벤트는 결코 준비되지 않아요.) thd가 실행되고 난 후 thread-resume-evt 호출 후에 일시중단되면 결과 이벤트는 준비된 채로 남아요. thd가 일시중단될 때마다 thread-resume-evt가 돌려줄 새 이벤트가 생성돼요. 이벤트의 결과는 thd이지만, thd가 결코 재개되지 않으면 이벤트에 대한 참조는 thd가 가비지 컬렉션되는 것을 막지 않아요 (Garbage Collection 문서 참고).

(thread-suspend-evt thd) → evt?
thd : thread?

procedure

thd가 일시중단될 때 동기화 준비가 되는 동기화 가능한 이벤트(Events 문서 참고)를 돌려줘요. (thd가 종료되었으면 이벤트는 결코 블록 해제되지 않아요.) thd가 일시중단된 후 thread-suspend-evt 호출 후에 재개되면 결과 이벤트는 준비된 채로 남아요. thd의 각 재개는 thread-suspend-evt가 돌려줄 새 이벤트를 만들어요. 이벤트의 결과는 thd이지만, thdthread로 만들어졌고(thread/suspend-to-kill이 아니라) 결코 재개되지 않으면 이벤트에 대한 참조는 thd가 가비지 컬렉션되는 것을 막지 않아요 (Garbage Collection 문서 참고).

thdthread/suspend-to-kill로 만들어졌으면 (thread-suspend-evt thd)를 기다리는 것은 thread로 만들어진 another-thd에 대한 (thread-dead-evt another-thd)와 같은 방식으로 기다리는 스레드의 가비지 컬렉션을 막아요. 또한 이벤트 결과가 thd이므로 (thread-suspend-evt thd)를 기다리는 것은 thd의 가비지 컬렉션을 막아요.

Thread Mailboxes

각 스레드에는 임의의 메시지를 받을 수 있는 사서함(mailbox)이 있어요. 즉 각 스레드에는 내장된 비동기 채널이 있어요.

Buffered Asynchronous Channels 문서도 참고하세요.

(thread-send thd v [fail-thunk]) → any
thd        : thread?
v          : any/c
fail-thunk : (or/c (-> any) #f)
           = (lambda () (raise-mismatch-error ....))

procedure

v를 블록 없이 thd에게 메시지로 큐에 넣어요. 메시지가 큐에 들어가면 결과는 #<void>예요. 메시지가 큐에 들어가기 전에 thd가 실행을 멈추면 — thread-running?에서처럼 — fail-thunk가 프로시저라면 (꼬리 호출로) 호출되어 결과를 만들고, fail-thunk#f이면 #f가 돌려져요.

(thread-receive) → any/c

procedure

현재 스레드를 위해 큐에 들어간 메시지가 있으면 그것을 받아 큐에서 빼요. 메시지가 없으면 thread-receive는 하나가 생길 때까지 블록돼요.

(thread-try-receive) → any/c

procedure

현재 스레드를 위해 큐에 들어간 메시지가 있으면 그것을 받아 큐에서 빼고, 메시지가 없으면 즉시 #f를 돌려줘요.

(thread-receive-evt) → evt?

procedure

동기화하는 스레드가 받을 메시지를 가질 때 동기화 준비가 되는 상수 동기화 가능한 이벤트(Events 문서 참고)를 돌려줘요. thread-receive 이벤트의 동기화 결과는 thread-receive 이벤트 자신이에요.

(thread-rewind-receive lst) → void?
lst : list?

procedure

lst의 요소들을 현재 스레드의 큐 앞쪽으로 다시 밀어 넣어요. 요소들은 하나씩 밀어 넣어지므로, 첫 번째 사용 가능한 메시지는 lst의 마지막 요소예요.

Parallel Thread Pools

(parallel-thread-pool? v) → thread?
v : any/c

procedure

v가 병렬 스레드 풀이면 #t, 그렇지 않으면 #f를 돌려줘요.

base 패키지 8.18.0.2 버전에서 추가되었어요.

(make-parallel-thread-pool [n]) → parallel-thread-pool?
n : exact-positive-integer? = (processor-count)

procedure

병렬 스레드를 다른 병렬 스레드들과 묶는 데 사용할 수 있는 병렬 스레드 풀을 만들어요. 풀의 스레드들은 실행에 최대 n개의 프로세서를 사용할 수 있어요. 풀에 n개보다 많은 스레드가 있으면 모두가 병렬로 실행되지는 않지만, 여전히 모두 동시에 실행돼요.

새 스레드 풀은 현재 커스토디언의 관리 아래 들어가요. 커스토디언이 종료되면 풀은 parallel-thread-pool-close와 같은 방식으로 닫혀요. 이미 풀에 있던 모든 스레드는 계속 실행되고 풀의 자원을 사용할 수 있어요 (같은 커스토디언에 의해 종료되지 않는 한).

병렬 스레드 풀은 BC 변형의 Racket이나 병렬성 지원 없이 컴파일된 Racket에서는 스레드를 병렬로 실행할 수 없어요. 그 경우 병렬 스레드는 코루틴 스레드와 똑같이 동작해요. CS 변형의 Racket에서는 futures-enabled? 술어를 사용해 병렬 스레드가 코루틴 스레드와 다르게 동작하는 때를 감지할 수 있어요.

base 패키지 8.18.0.2 버전에서 추가되었어요.

(parallel-thread-pool-close p) → void?
p : parallel-thread-pool?

procedure

병렬 스레드 풀을 닫아 더 이상 어떤 스레드도 풀에 추가될 수 없게 해요. 풀에 이미 있는 스레드들은 계속 실행이 허용되고 풀의 프로세서 자원을 계속 공유해요.

닫힌 스레드 풀 안에서 더 이상 실행 중인 스레드가 없거나, 풀의 커스토디언이 종료되어 어떤 스레드도 진행이 허용되지 않을 때, 풀에 할당된 프로세서 자원은 운영체제로 반환될 수 있어요 (즉 풀에 할당된 운영체제 스레드가 종료돼요).

base 패키지 8.18.0.2 버전에서 추가되었어요.

더 알아보기