Agents
Agents
가변 상태를 여러 스레드가 함께 써야 할 때, Clojure는 Refs만 있는 게 아니에요. Refs가 "여러 위치의 상태를 한꺼번에 바꾸는" 방법이라면, Agents는 "각 위치의 상태를 독립적으로 바꾸는" 방법이에요. 한 위치의 상태만 비동기로 다루고 싶을 때, 이 Agents를 쓰면 돼요.
출처: Clojure 공식문서
본문
Agents도 Refs처럼 가변 상태에 대한 공유 접근을 제공해요. 다만 Refs가 여러 위치의 조율된(coordinated)·동기(synchronous) 변경을 지원한다면, Agents는 개별 위치의 독립적·비동기 변경을 지원하죠. Agents는 평생 동안 하나의 저장 위치에 묶여 있고, 그 위치만 액션(action)의 결과로 변경할 수 있어요.
액션은 (옵션으로 추가 인자를 가진) 함수예요. 이 함수가 Agent의 상태에 비동기로 적용되고, 그 반환값이 Agent의 새 상태가 돼요. 액션이 함수이기 때문에 멀티메서드(multimethod)일 수도 있고, 그래서 액션은 다형적(polymorphic)일 수 있어요. 또 함수의 집합은 열려 있으니, Agent가 지원하는 액션의 집합도 열려 있어요. 이는 다른 언어에서 메시지 처리 루프를 pattern matching으로 다루는 방식과 크게 대비되는 점이에요.
Clojure의 Agents는 **반응형(reactive)**이지 자율적(autonomous)이지 않아요. 명령형 메시지 루프도 없고, 블로킹하는 receive도 없어요. Agent의 상태는 그 자체로 불변(immutable)이어야 하며, 가능하면 Clojure의 영속 컬렉션 중 하나의 인스턴스가 좋아요. 그리고 Agent의 상태는 어떤 메시지 없이도, 즉 관찰(observation)이 협력이나 조율을 요구하지 않고, 언제나 어떤 스레드에서든 즉시 읽을 수 있어요. (deref 함수 또는 @ 리더 매크로를 쓰면 되요.)
Agent 액션 디스패치는 (send agent fn args*) 형태예요. send(그리고 send-off)는 항상 즉시 반환해요. 조금 뒤, 다른 스레드에서 다음 일이 벌어져요.
- 주어진
fn이 Agent의 상태 와 인자(있다면)에 적용돼요. fn의 반환값이, Agent에 설정된 validator 함수가 있다면 그 함수로 전달돼요. 자세한 건set-validator!문서를 보면 돼요.- validator가 통과했거나 validator가 없으면,
fn의 반환값이 Agent의 새 상태가 돼요. - Agent에 추가된 watcher가 있다면 호출돼요. 자세한 건
add-watch문서를 참고해요. - 함수 실행 중 다른 디스패치가 (직접이든 간접이든) 일어났다면, 그 디스패치는 Agent의 상태가 변경된 이후 까지 보류돼요.
액션 함수가 예외를 던지면 중첩 디스패치는 일어나지 않고, 예외는 Agent 자체에 캐시돼요. Agent에 오류가 캐시되어 있으면, 오류가 지워질 때까지 이후의 모든 상호작용이 즉시 예외를 던져요. Agent 오류는 agent-error로 확인할 수 있고, restart-agent로 Agent를 재시작할 수 있어요.
모든 Agent의 액션은 스레드 풀 안의 스레드들 사이에 인터리브(interleaved)돼요. 어느 시점에서든 Agent 하나당 최대 하나의 액션만 실행돼요. 어떤 단일 Agent나 스레드에서 한 Agent로 디스패치된 액션들은 보낸 순서대로 실행되지만, 다른 소스에서 같은 Agent로 디스패치된 액션들과 인터리브될 수 있어요. send는 CPU 위주 액션에 쓰고, send-off는 IO에서 블로킹할 수 있는 액션에 적합해요.
Agents는 STM과 통합돼 있어요. 트랜잭션 안에서 만든 디스패치는 트랜잭션이 커밋될 때까지 보류되고, 재시도되거나 중단되면 버려져요.
Clojure의 다른 동시성 지원과 마찬가지로, 사용자 코드의 락(lock)은 관여하지 않아요.
참고로 Agent를 사용하면 non-daemon 백그라운드 스레드 풀이 시작돼 JVM 종료를 막아요. 이 스레드들을 종료하고 JVM이 꺼질 수 있게 하려면 shutdown-agents를 사용해야 해요.
예제
아래 예제는 "send-a-message-around-a-ring" 테스트의 구현이에요. m개의 Agent로 사슬을 만들고, n개의 액션 시퀀스를 사슬의 머리(head)에 디스패치한 뒤 그 사슬을 통해 중계(relay)해요.
(defn relay [x i]
(when (:next x)
(send (:next x) relay i))
(when (and (zero? i) (:report-queue x))
(.put (:report-queue x) i))
x)
(defn run [m n]
(let [q (new java.util.concurrent.SynchronousQueue)
hd (reduce (fn [next _] (agent {:next next}))
(agent {:report-queue q}) (range (dec m)))]
(doseq [i (reverse (range n))]
(send hd relay i))
(.take q)))
; 1 million message sends:
(time (run 1000 1000))
->"Elapsed time: 2959.254 msecs"
더 알아보기
- Agent 만들기:
agent - Agent 조사하기:
deref(그리고@리더 매크로),agent-error,error-handler,error-mode - Agent 상태 변경:
send,send-off,restart-agent - Agent 대기(블로킹):
await,await-for - Ref validator:
set-validator!,get-validator - Watcher:
add-watch,remove-watch - Agent 스레드 관리:
shutdown-agents - Agent 오류 관리:
agent-error,restart-agent,set-error-handler!,error-handler,set-error-mode!,error-mode