플레이스
플레이스 (Places)
플레이스(place)는 여러 프로세서·코어·하드웨어 스레드를 가진 머신의 병렬성을 활용하는 병렬 프로그램을 만들 수 있게 해주는 메커니즘이에요. 각 플레이스는 사실상 Racket 가상 머신의 별도 인스턴스로 동작하며, 플레이스 채널(place channel)로 서로 통신해요.
출처: Racket Reference
본문
Racket Guide의 Parallelism with Places에서 플레이스를 소개해요.
(require racket/place) ; package: base
(require racket/place/dynamic)
이 절에서 다루는 바인딩들은 racket/base가 아닌 racket/place, racket/place/dynamic, racket 라이브러리에서 제공돼요.
플레이스는 여러 프로세서·코어·하드웨어 스레드를 가진 머신을 활용하는 병렬 프로그램의 개발을 가능하게 해요.
현재 플레이스의 병렬 지원은 Racket의 기본 구현인 Racket CS를 지원하는 모든 플랫폼에서 활성화돼요. 3m 구현도 Windows, Linux x86/x86_64, Mac OS x86/x86_64에서 플레이스의 병렬 실행을 기본으로 지원해요. 3m으로 다른 플랫폼의 지원을 활성화하려면 Racket을 빌드할 때 configure와 함께 --enable-places를 사용해요. place-enabled? 함수가 플레이스가 병렬로 실행되는지 보고해요.
구현과 운영 체제의 제약이 플레이스의 확장성을 제한할 수 있어요. 예를 들어 플레이스는 CS 구현에서 병렬로, 3m 구현에서 독립적으로 가비지 컬렉션을 수행할 수 있지만, 가비지 컬렉션이 모든 플레이스에 공유되는 페이지 테이블을 조작해야 할 수 있고, 그 공유 페이지 테이블은 플레이스가 충분히 많을 때—아마 8이나 16개쯤—병목이 될 수 있어요.
플레이스는 사실상 Racket 가상 머신의 별도 인스턴스인 병렬 태스크예요. 다만 모든 플레이스는 단일 운영 체제 프로세스 안에서 실행돼요. 플레이스는 양방향 버퍼링 통신의 끝점인 플레이스 채널을 통해 통신해요.
대략적으로 말하면 플레이스 채널은 불변·투명한 값만 메시지로 지원해요. 게다가 플레이스 채널 자체를 채널을 통해 보내서 기존 통신선에 더해 새(어쩌면 더 직접적인) 통신선을 만들 수 있어요. 마지막으로 shared-flvector, make-shared-flvector, shared-fxvector, make-shared-fxvector, shared-bytes, make-shared-bytes가 만든 가변 값을 플레이스 채널을 통해 보낼 수 있어요. 그런 값의 변형은 그 값을 공유하는 모든 플레이스에 보이는데, 그 값이 공유 메모리 공간에 허용되기 때문이에요. place-message-allowed?를 참고하세요.
플레이스 채널은 채널을 통해 값을 받기 위한 동기화 가능 이벤트(Events 참고)로 사용될 수 있어요. 채널에 메시지가 있을 때 플레이스 채널이 동기화 준비가 되고, 플레이스 채널의 동기화 결과는 그 메시지(동기화 시 제거됨)예요. 플레이스는 place-channel-get으로 메시지를 받을 수도 있고, place-channel-put으로 메시지를 보낼 수도 있어요.
두 플레이스 채널은 같은 기본 채널의 끝점이면서 둘 다 플레이스 기술자(descriptor)이거나 둘 다 아닐 때 equal?이에요. 플레이스 채널은 플레이스 채널로 메시지를 보낸 뒤 eq?가 아니면서 equal?일 수 있어요.
플레이스 채널을 통한 메시지에 대한 제약—따라서 플레이스들이 공유하는 데이터 종류에 대한 제약—은 future보다 더 큰 병렬성을 가능하게 해요. 개별 플레이스의 별도 가비지 컬렉션까지 포함해서요. 동시에 플레이스의 설정·통신 비용은 future보다 더 높을 수 있어요.
예를 들어 다음 표현식은 두 플레이스를 시작하고, 각각에 메시지를 메아리로 보낸 다음, 플레이스가 종료될 때까지 기다려요:
(let ([pls (for/list ([i (in-range 2)])
(dynamic-place "place-worker.rkt" 'place-main))])
(for ([i (in-range 2)]
[p pls])
(place-channel-put p i)
(printf "~a\n" (place-channel-get p)))
(for-each place-wait pls))
"place-worker.rkt" 모듈(위 코드와 분리된 파일 안에 있음)은 각 플레이스가 실행하는 place-main 함수를 export해야 해요. place-main은 단일 플레이스 채널 인자를 받아야 해요:
"place-worker.rkt":
#lang racket
(provide place-main)
(define (place-main pch)
(place-channel-put pch (format "Hello from place ~a"
(place-channel-get pch))))
플레이스 채널은 다른 Racket 값들처럼 가비지 컬렉션의 대상이고, 플레이스 채널에서 읽기를 막고 있는 스레드는 플레이스 채널의 쓰기 끝이 도달 불가능해지면 가비지 컬렉션될 수 있어요. 그러나 일반 채널 블로킹과 달리, 달리 도달 불가능한 스레드들이 같은 스레드들에서만 도달 가능한 플레이스 채널에서 서로 막혀 있으면, 그 스레드들과 플레이스 채널들은 모두 도달 불가능이 아니라 도달 가능한 것으로 간주돼요.
플레이스가 만들어질 때, 그 파라미터 값들은 일반적으로 만드는 플레이스의 파라미터 초기 값으로 설정돼요. 다만 다음 파라미터의 현재 값이 사용돼요: current-library-collection-paths, current-library-collection-links, current-compiled-file-roots.
새로 만들어진 플레이스는 현재 custodian에 등록되어, custodian이 종료되면 플레이스도 종료돼요.
11.5.1 플레이스 사용하기 (Using Places)
procedure
(place-enabled?) → boolean?
Racket이 dynamic-place와 place가 병렬로 실행될 수 있는 플레이스를 만드는 방식으로 구성됐으면 #t를, dynamic-place와 place가 thread로 시뮬레이션되면 #f를 돌려줘요.
procedure
(place? v) → boolean?
v : any/c
v가 플레이스 기술자 값이면 #t를, 그렇지 않으면 #f를 돌려줘요. 모든 플레이스 기술자는 플레이스 채널이기도 해요.
procedure
(place-channel? v) → boolean?
v : any/c
v가 플레이스 채널이면 #t를, 그렇지 않으면 #f를 돌려줘요.
procedure
(dynamic-place module-path start-name [#:at location #:named named]) → place?
module-path : (or/c module-path? path?)
start-name : symbol?
location : (or/c #f place-location?) = #f
named : any/c = #f
module-path와 start-name으로 식별되는 프로시저를 실행할 플레이스를 만들어요. 결과는 새 병렬 태스크를 나타내는 플레이스 기술자 값이고, 플레이스 기술자는 즉시 돌아와요. 플레이스 기술자 값은 플레이스와 통신할 수 있게 하는 플레이스 채널이기도 해요.
module-path가 가리키는 모듈은 이름이 start-name인 함수를 export해야 해요. 그 함수는 place가 돌려준 플레이스 기술자의 통신 반대편 끝에 해당하는 플레이스 채널 하나를 인자로 받아야 해요.
location이 주어지면 플레이스 위치여야 해요. 예를 들어 create-place-node가 만든 분산 플레이스 노드 같은 것예요.
플레이스가 만들어질 때, 초기 exit 핸들러가 플레이스를 종료하고, exit 핸들러 인자를 플레이스의 완료 값(completion value)으로 사용해요. (exit v)를 사용해 완료 값 v로 플레이스를 즉시 종료할 수 있어요. 완료 값은 0과 255 사이의 정확한 정수로 제한되므로, v의 다른 값은 0으로 변환돼요.
module-path와 start-name이 가리키는 함수가 돌아오면 플레이스는 완료 값 0으로 종료돼요.
만들어진 플레이스에서 current-input-port 파라미터는 빈 입력 포트로 설정되고, current-output-port와 current-error-port 파라미터의 값은 만드는 플레이스의 현재 포트에 연결돼요. 만드는 플레이스의 출력 포트가 파일 스트림 포트라면, 만들어진 플레이스의 연결된 포트들은 기본 스트림을 공유하고, 그렇지 않으면 만드는 플레이스의 스레드가 만들어진 플레이스의 포트에서 만드는 플레이스의 현재 포트로 바이트를 펌핑해요.
만들어진 플레이스의 대부분 파라미터는 원래 초기 값을 갖지만, 만들어진 플레이스는 다음 파라미터에 대해 만드는 플레이스의 값을 상속해요: current-directory, current-library-collection-paths, current-library-collection-links, current-compiled-file-roots.
module-path 인자는 모듈이 미리 정의되지 않은 한(module-predefined? 참고) (quote sym) 형태의 모듈 경로여서는 안 돼요.
dynamic-place 바인딩은 protect-out의 의미로 보호되므로, 코드 inspector를 조정해(Code Inspectors 참고) 이 연산에 대한 접근을 막을 수 있어요.
base 패키지의 8.2.0.7 버전에서 만들어진 플레이스가 만드는 플레이스의 current-directory 값을 상속하도록 변경했어요.
procedure
(dynamic-place* module-path start-name [#:in in #:out out #:err err])
→ place? (or/c output-port? #f) (or/c input-port? #f) (or/c input-port? #f)
module-path : (or/c module-path? path?)
start-name : symbol?
in : (or/c input-port? #f) = #f
out : (or/c output-port? #f) = (current-output-port)
err : (or/c output-port? #f) = (current-error-port)
dynamic-place와 같지만 새 플레이스의 포트에 연결할 특정 포트를 받아들이고, 어떤 포트에 #f를 주면 만들어진 포트를 돌려줘요. in, out, err 포트는 각각 플레이스의 current-input-port, current-output-port, current-error-port 포트에 연결돼요. 포트 중 아무거나 #f일 수 있고, 그 경우(운영 체제 파이프용) 파일 스트림 포트가 만들어져 dynamic-place*가 돌려줘요. err 인자는 'stdout일 수 있고, 그 경우 표준 출력으로 제공된 것과 같은 파일 스트림 포트를 표준 오류에도 사용해요. 제공된 각 포트나 'stdout에 대해 파이프는 만들어지지 않고 해당 반환 값은 #f예요.
dynamic-place*의 호출자는 돌려받은 모든 포트를 닫을 책임이 있어요. 어떤 것도 자동으로 닫히지 않아요.
dynamic-place* 프로시저는 네 값을 돌려줘요:
- 만들어진 플레이스를 나타내는 플레이스 기술자 값.
- 플레이스의 표준 입력에 파이프로 연결된 출력 포트.
in이 포트였다면#f. - 플레이스의 표준 출력에서 파이프로 연결된 입력 포트.
out이 포트였다면#f. - 플레이스의 표준 오류에서 파이프로 연결된 입력 포트.
err가 포트 또는'stdout이었다면#f.
dynamic-place* 바인딩은 dynamic-place와 같은 방식으로 보호돼요.
procedure
(place-wait p) → exact-integer?
p : place?
p가 나타내는 플레이스의 완료 값을 돌려주고, 플레이스가 종료될 때까지 블록해요.
p에 대해 비파일 스트림 포트를 플레이스의 포트에 연결하기 위해 펌핑 스레드가 만들어졌다면(dynamic-place 참고), place-wait는 그 펌핑 스레드들이 완료될 때만 돌아와요.
procedure
(place-dead-evt p) → evt?
p : place?
p가 종료됐을 때만 동기화가 준비되는 동기화 가능 이벤트(Events 참고)를 돌려줘요. place-dead 이벤트의 동기화 결과는 place-dead 이벤트 그 자체예요.
p에 대해 비파일 스트림 포트를 플레이스의 포트에 연결하기 위해 펌핑 스레드가 만들어졌다면(dynamic-place 참고), place-dead-evt가 돌려주는 이벤트는 펌핑 스레드가 여전히 실행 중이어도 준비될 수 있어요.
procedure
(place-kill p) → void?
p : place?
플레이스를 즉시 종료하고, 플레이스가 아직 완료 값을 갖고 있지 않으면 완료 값을 1로 설정해요.
procedure
(place-break p [kind]) → void?
p : place?
kind : (or/c #f 'hang-up 'terminate) = #f
플레이스 p의 메인 스레드에 break를 보내요. Breaks를 참고하세요.
procedure
(place-channel) → place-channel? place-channel?
두 개의 플레이스 채널을 돌려줘요. 첫 채널을 통해 보낸 데이터는 두 번째 채널을 통해 받을 수 있고, 두 번째 채널을 통해 보낸 데이터는 첫 채널에서 받을 수 있어요.
보통 한 플레이스 채널은 현재 플레이스가 대상 플레이스에 메시지를 보내는 데 사용되고, 다른 플레이스 채널은 (기존 플레이스 채널을 통해) 대상 플레이스로 보내져요.
procedure
(place-channel-put pch v) → void
pch : place-channel?
v : place-message-allowed?
채널 pch에 메시지 v를 보내요. 플레이스 채널은 비동기적이라 place-channel-put 호출은 논블로킹이에요.
v에서 가변 문자열을 불변 문자열로 변환하는 것 같은 자동 변환(coercion)에 대한 정보는 place-message-allowed?를 참고하세요.
procedure
(place-channel-get pch) → place-message-allowed?
pch : place-channel?
채널 pch에서 받은 메시지를 돌려주고, 메시지를 사용할 수 있을 때까지 블록해요.
procedure
(place-channel-put/get pch v) → any/c
pch : place-channel?
v : any/c
채널 pch에 불변 메시지 v를 보낸 다음 같은 채널에서 (어쩌면 응답인) 메시지를 기다려요.
procedure
(place-message-allowed? v) → boolean?
v : any/c
v가 플레이스 채널의 메시지로 허용되면 #t를, 그렇지 않으면 #f를 돌려줘요.
(place-enabled?)가 #f를 돌려주면 결과는 항상 #t이고 v에 어떤 변환도 수행되지 않아요. 그렇지 않으면 다음 종류의 데이터가 메시지로 허용돼요:
- 숫자, 문자, 불리언, 키워드,
#<void>. - 심볼. 여기서 미내부화(uninterned) 심볼의
eq?동일성은 단일 메시지 안에서 보존되지만 메시지 간에는 보존되지 않아요. - 문자열과 바이트 문자열. 여기서 가변 문자열·바이트 문자열은 자동으로 불변 변형으로 대체돼요.
- 경로(어떤 플랫폼이든).
- 메시지 허용 값을 담은 페어, 리스트, 박스, 벡터, 불변 prefab 구조체. 여기서 가변 박스는 자동으로 불변 박스로, 가변 벡터는 자동으로 불변 벡터로 대체되고, 박스·벡터·prefab 구조체의 impersonator는 복사돼요.
- 해시 테이블. 여기서 가변 해시 테이블은 자동으로 불변 변형으로 대체되고, 해시 테이블 impersonator는 복사돼요.
- 플레이스 채널. 여기서 플레이스 기술자는 자동으로 일반 플레이스 채널로 대체돼요.
- 파일 스트림 포트와 TCP 포트. 여기서 기본 표현(파일 디스크립터, 소켓, 핸들 등)이 보내는 플레이스에서 중복되고 수신 플레이스의 새 포트에 붙어요.
ffi/unsafe로 만들거나 접근한 C 포인터.shared-flvector,make-shared-flvector,shared-fxvector,make-shared-fxvector,shared-bytes,make-shared-bytes가 만든 값.
base 패키지의 8.4.0.7 버전에서 허용 메시지에 박스를 포함했어요.
value
prop:place-location : struct-type-property?
procedure
(place-location? v) → boolean?
v : any/c
플레이스 위치 구현을 위한 구조체 타입 속성과 연관된 술어예요. prop:place-location의 값은 네 인자를 받는 프로시저여야 해요: 플레이스 위치 자체, 모듈 경로, 모듈이 export하는 시작 함수의 심볼, 플레이스 이름(익명 플레이스는 #f일 수 있음).
플레이스 위치는 dynamic-place의 #:at 인자로 전달될 수 있고, 전달하면 그 플레이스 위치의 prop:place-location 값을 호출해요.
create-place-node로 만든 분산 플레이스 노드는 플레이스 위치의 예예요.
11.5.2 플레이스 사용을 위한 문법 지원 (Syntactic Support for Using Places)
이 절의 바인딩들은 racket/place/dynamic에서 제공되지 않아요.
syntax
(place id body ...+)
id가 플레이스 채널에 바인딩된 채로 body 표현식들을 평가하는 플레이스를 만들어요. bodys는 서브모듈로 들어 올려지므로, bodys는 id와 둘러싼 모듈의 최상위 바인딩만 닫아요. place의 결과는 dynamic-place의 결과처럼 플레이스 기술자예요.
생성된 서브모듈은 정수 n에 대해 place-body-n이라는 이름을 갖고, 서브모듈은 새 플레이스용 플레이스 채널을 받는 main 함수를 export해요. 그러나 그 서브모듈은 place 폼의 확장 외에는 사용할 의도가 아니에요.
place 바인딩은 dynamic-place와 같은 방식으로 보호돼요.
syntax
(place* maybe-port ...
id
body ...+)
maybe-port =
| #:in in-expr
| #:out out-expr
| #:err err-expr
place와 같지만 dynamic-place*와 같은 방식과 같은 기본값으로 포트를 지정하는 선택적 #:in, #:out, #:err 표현식(각각 최대 하나)을 지원해요. place* 폼의 결과도 dynamic-place*와 같아요.
place* 바인딩은 dynamic-place와 같은 방식으로 보호돼요.
syntax
(place/context id body ...+)
place와 같지만 body ...는 자유 어휘 변수를 가질 수 있고, 그 변수들은 자동으로 새로 만들어진 플레이스로 보내져요. 이 변수들은 place-message-allowed?가 받아들이는 값을 가져야 하며, 그렇지 않으면 exn:fail:contract 예외가 발생해요.
procedure
(processor-count) → exact-positive-integer?
현재 머신에서 사용 가능한 병렬 계산 단위(예: 프로세서나 코어)의 수를 돌려줘요.
racket/future에서도 제공되는 것과 같은 바인딩이에요.
11.5.3 플레이스 로깅 (Places Logging)
플레이스 이벤트는 'place라는 이름의 로거에 보고돼요. 플레이스에 대해 로그되는 각 이벤트는 문자열 메시지 외에, place-event prefab 구조체의 인스턴스인 데이터 값을 가져요:
(struct place-event (place-id action value time)
#:prefab)
place-id 필드는 플레이스를 식별하는 정확한 정수예요.
time 필드는 current-inexact-milliseconds와 같은 방식으로 시간을 나타내는 부정확한 숫자예요.
action 필드는 심볼이에요:
-
'create: 플레이스가 만들어졌어요. 이 이벤트는 만드는 플레이스에서 로그되고, 이벤트의value필드는 만들어진 플레이스의 ID를 가져요. -
'reap: 현재 플레이스에서 이전에 만들어진 플레이스가 종료됐어요(그 사실이, 어쩌면place-wait를 통해 감지됐어요). 이벤트의value필드는 종료된 플레이스의 ID를 가져요. -
'enter: 플레이스가 시작됐어요. 시작된 플레이스 안에서 로그돼요. 이벤트의value필드는#f예요. -
'exit: 플레이스가 종료되고 있어요. 종료되는 플레이스 안에서 로그돼요. 이벤트의value필드는#f예요. -
'put: 플레이스 채널 메시지가 보내졌어요. 이벤트의value필드는 메시지의 크기를 근사한 양의 정확한 정수예요. -
'get: 플레이스 채널 메시지가 받아졌어요. 이벤트의value필드는 메시지의 크기를 근사한 양의 정확한 정수예요.
base 패키지의 6.0.0.2 버전에서 'place와 place-event를 통한 로깅을 추가했어요.
더 알아보기
- Racket Guide: Parallelism with Places — 플레이스 병렬성 소개.
- Futures — 플레이스보다 가벼운 병렬성 메커니즘.