퓨처(Futures)

퓨처(Futures)

future는 Racket이 하드웨어가 지원하는 진짜 병렬성(parallelism)을 활용하는 방법이에요. 스레드의 동시성과 달리 여러 프로세서 코어에서 실제로 동시에 작업이 돌아갈 수 있습니다.

출처: Racket Reference

본문

퓨처를 통한 병렬성에 대한 소개는 The Racket Guide의 Parallelism with Futures에서 다룹니다.

(require racket/future)    ; package: base

이 절에서 문서화하는 바인딩은 racket/base가 아니라 racket/futureracket 라이브러리가 제공합니다.

future를 통한 병렬성 지원은 보통 Racket의 CS 구현체에서 모든 플랫폼에 대해 활성화되어 있습니다. BC 구현체에서는 Windows, Linux x86/x86_64, Mac OS x86/x86_64에 대해 병렬성 지원이 기본으로 활성화됩니다. Racket BC를 빌드하는 다른 플랫폼에서 지원을 활성화하려면 configure와 함께 --enable-futures를 사용하세요.

racket/futurefuturetouch 함수는 하드웨어와 운영체제가 지원하는 병렬성에 접근할 수 있게 해줍니다. 임의의 계산에 대해 병렬성 없이 동시성(concurrency)을 제공하는 thread와는 대조적으로, future는 병렬성을 제공합니다. 퓨처는 (병렬성 지원이 가능하다는 전제하에) 병렬로 안전하게 실행할 수 없는 연산을 시도하는 것을 감지할 때까지 그 작업을 병렬로 실행합니다. 마찬가지로, 예외를 발생시키는 것처럼 현재 연속(continuation)에 어떤 식으로든 의존하면 퓨처의 작업은 중단됩니다. 퓨처를 중단시키는 연산들은 차단 연산(blocking operation)입니다. 퓨처의 중단된 계산은 touch가 그 퓨처에 적용될 때 재개됩니다.

퓨처의 "안전한" 병렬 실행은 시스템이 제공하는 모든 연산이 계약을 강제하고 문서화된 대로 결과를 만들어낼 수 있어야 한다는 뜻입니다. "안전함"이 프로그램에 보이는 가변 데이터에 대한 동시 접근을 배제하지는 않습니다. 예를 들어, 퓨처의 계산이 set!로 공유 변수를 수정할 수 있는데, 이 경우 변수에 대한 동시 할당이 다른 퓨처와 스레드에 보일 수 있습니다. 게다가 효과의 가시성과 순서에 대한 보장은 운영체제와 하드웨어에 의해 결정됩니다—그것들은 거의 예를 들어 스레드 기반 동시성에서 제공되는 순차 일관성(sequential consistency)의 보장을 지원하지 않습니다. Machine Memory Order도 함께 보세요. 겉보기에 분명히 안전해 보이는 시스템 연산도 내부 구현이 병렬로 실행될 수 없는 경우가 있습니다. 시스템 연산의 동작을 이해하려면 The Racket Guide의 Parallelism with Futures를 더 자세히 읽어보고 future-visualizer를 사용하는 소개를 참고하세요.

퓻처를 생성한 스레드가 실행되도록 허용하는 모든 custodian이 종료되면 퓨처는 절대 병렬로 실행되지 않습니다. 그러나 그런 퓨처는 touch 호출을 통해 실행될 수 있습니다.

퓨처 생성·터치(Creating and Touching Futures)

procedure

(future thunk) → future?
  thunk : (-> any)

(touch f) → any
  f : future?

future 프로시저는 thunk을 캡슐화하는 퓨처 값(future value)을 반환합니다. touch 함수는 주어진 퓨처 안에서 thunk의 평가를 강제하고, thunk가 만들어내는 값들을 반환합니다. touchthunk의 평가를 강제한 뒤에는 결과 값들이 thunk 대신 퓨처에 유지되고, 퓨처에 대한 추가적인 touch는 그 값들을 반환합니다.

주어진 퓨처에 대해 future 호출과 touch 호출 사이에서, 그 thunk는 위에서 설명한 대로 다른 계산들과 병렬로 추측적으로(speculatively) 실행될 수 있습니다.

예시:

> (let ([f (future (lambda () (+ 1 2)))])
    (list (+ 3 4) (touch f)))
'(7 3)
procedure

(futures-enabled?) → boolean?

현재 Racket 설정에서 퓨처 병렬 지원이 활성화되어 있는지 반환합니다.

procedure

(current-future) → (or/c #f future?)

현재 연속인 thunk 실행을 가진 퓨처의 descriptor를 반환합니다. 즉, 퓨처 descriptor f가 반환되면 (touch f)가 현재 연속의 결과를 만들어낼 것입니다. 퓨처 thunk 자체가 touch를 사용하면 future-thunk 실행이 중첩될 수 있는데, 이 경우 가장 즉시 실행 중인 퓨처의 descriptor가 반환됩니다. 현재 연속이 어떤 퓨처의 touch로 돌아가지 않으면 결과는 #f입니다.

procedure

(future? v) → boolean?
  v : any/c

v가 퓨처 값이면 #t, 그 외에는 #f를 반환합니다.

procedure

(would-be-future thunk) → future?
  thunk : (-> any)

절대 병렬로 실행되지 않지만, 퓨처의 thunk 실행 동안 잠재적으로 "안전하지 않은" 연산(즉 병렬 실행을 방해하는 연산)을 일관되게 모두 기록(log)하는 퓨처를 반환합니다.

일반 퓨처에서는 어떤 상황 때문에 안전하지 않은 연산의 기록이 막힐 수 있습니다. 예를 들어 디버그 수준 로깅으로 실행할 때,

(touch (future (lambda ()
                 (printf "hello1")
                 (printf "hello2")
                 (printf "hello3"))))

는 각 printf 호출에 대해 하나씩 세 개의 메시지를 기록할 수 있습니다. 하지만 touch가 퓨처가 병렬로 실행을 시작할 기회를 갖기 전에 수행되면, 퓨처 thunk는 여느 일반 thunk와 같은 방식으로 평가되고 안전하지 않은 연산이 기록되지 않습니다. futurewould-be-future로 바꾸면 printf의 세 호출 모두가 기록됨을 보장합니다.

procedure

(processor-count) → exact-positive-integer?

현재 머신에서 사용 가능한 병렬 계산 단위(예: 프로세서 또는 코어)의 개수를 반환합니다.

이것은 racket/place에서 사용할 수 있는 것과 같은 바인딩입니다.

syntax

(for/async (for-clause ...) body-or-break ... body)

(for*/async (for-clause ...) body-or-break ... body)

forfor*와 같지만, body의 각 반복이 별도의 퓨처에서 실행되고, 퓨처들은 어떤 순서로든 touch될 수 있습니다.

퓨처 세마포어(Future Semaphores)

procedure

(make-fsemaphore init) → fsemaphore?
  init : exact-nonnegative-integer?

내부 카운터가 처음에 init으로 설정된 새 퓨처 세마포어를 만들고 반환합니다.

퓨처 세마포어는 일반 세마포어와 비슷하지만, 미래 세마포어 연산은 (병렬 계산을 동기화하기 위해) 병렬로 안전하게 수행될 수 있습니다. 대조적으로 일반 세마포어 연산은 병렬로 수행해도 안전하지 않으므로, 계산이 병렬로 계속되는 것을 막습니다.

fsemaphore로 잠금(lock)을 구현하려고 하지 마세요. 퓨처는 다른 퓨처와 동시적·병렬적으로 실행될 수 있지만, Racket 스레드가 요구하지 않는 퓨처는 언제든 중단될 수 있습니다—예를 들어 잠금을 취한 직후, 잠금을 해제하기 전에. 퓨처들 사이에서 가변 데이터를 공유해야 한다면, 잠금 없는 데이터 구조(lock-free data structures)가 일반적으로 더 잘 맞습니다.

procedure

(fsemaphore? v) → boolean?
  v : any/c

v가 퓨처 세마포어 값이면 #t, 그 외에는 #f를 반환합니다.

procedure

(fsemaphore-post fsema) → void?
  fsema : fsemaphore?

퓨처 세마포어의 내부 카운터를 증가시키고 #<void>를 반환합니다.

procedure

(fsemaphore-wait fsema) → void?
  fsema : fsemaphore?

fsema의 내부 카운터가 0이 아닐 때까지 차단합니다. 카운터가 0이 아닐 때, 그것은 감소되고 fsemaphore-wait#<void>를 반환합니다.

procedure

(fsemaphore-try-wait? fsema) → boolean?
  fsema : fsemaphore?

fsemaphore-wait와 같지만, fsemaphore-try-wait?는 절대 실행을 차단하지 않습니다. fsema의 내부 카운터가 0이면 fsemaphore-try-wait?는 카운터를 감소시키지 않고 즉시 #f를 반환합니다. fsema의 카운터가 양수이면 감소되고 #t가 반환됩니다.

procedure

(fsemaphore-count fsema) → exact-nonnegative-integer?
  fsema : fsemaphore?

fsema의 현재 내부 카운터 값을 반환합니다.

퓨처 성능 로깅(Future Performance Logging)

Racket 추적(trace)은 퓨처가 어떻게 평가되는지에 대한 정보를 보고하기 위해 로깅(Logging 참고)을 광범위하게 사용합니다. 로깅 출력은 퓨처를 사용하는 프로그램의 성능을 디버깅하는 데 유용합니다.

텍스트 로그 출력을 직접 보거나(코드에서 trace-futures로 검색하거나) future-visualizer가 제공하는 그래픽 프로파일러 도구를 사용하는 것이 훨씬 쉽습니다.

퓨처 이벤트는 'future 토픽으로 로깅됩니다. 문자열 메시지에 더해, 퓨처에 대해 로깅된 각 이벤트는 future-event prefab 구조체의 인스턴스인 데이터 값을 갖습니다:

(struct future-event (future-id proc-id action time prim-name user-data)
  #:prefab)

future-id 필드는 퓨처를 식별하는 정확한 정수이며, action'missing일 때는 #f입니다. future-id 필드는 로깅된 이벤트를 서로 연관 짓는 데 특히 유용합니다.

proc-id 필드는 병렬 프로세스를 식별하는 정확한 음이 아닌 정수입니다. 프로세스 0은 주 Racket 프로세스로, future thunk 이외의 모든 표현식이 그곳에서 평가됩니다.

time 필드는 current-inexact-milliseconds와 같은 방식으로 시간을 나타내는 부정확한 숫자입니다.

action 필드는 심볼입니다:

  • 'create: 퓨처가 생성되었습니다.
  • 'complete: 퓨처의 thunk가 성공적으로 평가되어 touch가 그 퓨처에 대한 값을 즉시 만들어낼 것입니다.
  • 'start-work'end-work: 특정 프로세스가 특정 퓨처에 대한 작업을 시작하고 끝냈습니다.
  • 'start-0-work: 'start-work와 같지만, 어떤 구조적 이유로 프로세스 0이 아닌 프로세스에서 시작될 수 없었던 future thunk용입니다(예: thunk가 시작하기에 너무 많은 로컬 저장소를 요구하는 경우).
  • 'start-overflow-work: 'start-work와 같지만, future thunk의 작업이 내부 스택 오버플로로 이전에 중단되었던 경우입니다.
  • 'sync: future thunk의 평가에서 "안전하지 않은" 연산에 대한 (프로세스 0이 아닌 프로세스의) 차단 또는 (프로세스 0의) 넘겨주기(handoff) 시작입니다. 그 연산은 프로세스 0에서 실행되어야 합니다.
  • 'block: 'sync와 같지만, 평가가 현재 연속에 의존할 수 있기 때문에 퓨처가 touche될 때까지 지연되어야 하는 평가 부분에 대한 것입니다.
  • 'touch(프로세스 0에서는 절대 없음): 'sync'block과 같지만, future thunk 안의 touch 연산에 대한 것입니다.
  • 'overflow(프로세스 0에서는 절대 없음): 'sync'block과 같지만, 프로세스가 future thunk를 평가하는 동안 내부 스택 오버플로를 만난 경우에 대한 것입니다.
  • 'result 또는 'abort: 'sync, 'block, 'touch에 대한 대기 또는 처리가 각각 값이나 오류로 끝났습니다.
  • 'suspend(프로세스 0에서는 절대 없음): 프로세스가 'sync, 'block, 'touch에 의해 차단된 퓨처의 평가를 포기했습니다. 다른 프로세스가 나중에 그 퓨처를 집어들 수 있습니다.
  • 'touch-pause'touch-resume(프로세스 0에서만): thunk가 다른 프로세스에서 평가 중인 퓨처에 대해 touch에서 대기하는 것에 대한 것입니다.
  • 'missing: 프로세스에 대한 하나 이상의 이벤트가 보고되기 전에 내부 버퍼 한도 때문에 유실되었고, time-id 필드가 유실된 이벤트들의 시간에 대한 상한을 보고합니다. 이런 이벤트는 드뭅니다.

'missing 이벤트가 없다고 가정하면, 'start-work, 'start-0-work, 'start-overflow-work는 항상 'end-work와 쌍을 이루고, 'sync, 'block, 'touch는 항상 'result, 'abort, 'suspend와 쌍을 이루며, 'touch-pause는 항상 'touch-resume과 쌍을 이룹니다.

프로세스 0에서 일부 이벤트 쌍은 다른 이벤트 쌍 안에 중첩될 수 있습니다: 'result'abort를 가진 'sync, 'block, 'touch; 'touch-resume을 가진 'touch-pause; 그리고 'end-work를 가진 'start-work.

프로세스 0에서 'block은 안전하지 않은 연산이 처리될 때 생성됩니다. 이 유형의 이벤트는 연산의 이름인 unsafe-op-name 필드에 심볼을 담게 됩니다. 다른 모든 경우에 이 필드는 #f를 담습니다.

prim-name 필드는 이벤트가 프로세스 0에서 발생하고 그 action이 'block이나 'sync가 아니라면 항상 #f입니다. 이런 조건이 충족되면 prim-name은 퓨처가 (심볼로 표시된) 런타임 스레드와 동기화하도록 요구한 Racket 원시 함수의 이름을 담습니다.

user-data 필드는 actionprim-name 필드 둘 다에 따라 여러 다른 값을 취할 수 있습니다:

  • 프로세스 0에서의 'touch: touch되고 있는 퓨처의 정수 ID를 담습니다.
  • 'sync이고 prim-name'|allocate memory|인 경우: 요청된 할당의 크기(바이트)입니다.
  • 'sync이고 prim-name'jit_on_demand인 경우: 런타임 스레드가 퓨처 future-id를 대신해 JIT 컴파일을 수행하고 있습니다. 이 필드는 JIT 컴파일되고 있는 함수의 이름(심볼로)을 담습니다.
  • 'create: 새 퓨처가 생성되었습니다. 이 필드는 새로 생성된 퓨처의 정수 ID를 담습니다.

더 알아보기