Refs와 트랜잭션

Refs와 트랜잭션 (Refs and Transactions)

Var가 스레드 격리(isolation) 를 통해 변경 가능한 저장 위치의 안전한 사용을 보장한다면, 트랜잭션 레퍼런스(Ref)는 소프트웨어 트랜잭션 메모리(STM) 시스템을 통해 변경 가능한 저장 위치의 안전한 공유 사용을 보장해요. Ref는 수명 동안 단일 저장 위치에 바인딩되고, 그 위치의 변경이 트랜잭션 안에서만 일어나도록 허용해요.

데이터베이스 트랜잭션을 써 봤다면 Clojure 트랜잭션은 이해하기 쉬워요 — Ref에 대한 모든 동작이 원자적(atomic), 일관적(consistent), 격리적(isolated)임을 보장하거든요. 원자적이란 트랜잭션 안에서 Ref에 대한 모든 변경이 일어나거나 아무것도 일어나지 않는다는 뜻이에요. 일관적이란 트랜잭션이 커밋되기 전에 각 새 값을 검증기(validator) 함수로 확인할 수 있다는 뜻이에요. 격리적이란 어떤 트랜잭션도 실행 중에 다른 트랜잭션의 효과를 보지 못한다는 뜻이에요. STM의 또 다른 공통 특징은, 트랜잭션이 실행 중 충돌이 있으면 자동으로 재시도된다는 점이에요.

STM을 구현하는 방법은 여러 가지가 있고(잠금/비관적, 잠금 없음/낙관적, 하이브리드), 여전히 연구 문제예요. Clojure STM은 다중 버전 동시성 제어(MVCC)와 스냅샷 격리를 위한 적응형 기록 큐(adaptive history queue)를 사용하며, 구별되는 commute 연산을 제공해요.

실제로는 다음을 의미해요:

. Ref에 대한 모든 읽기는 트랜잭션의 시작점(그 '읽기 지점')을 기준으로 한 'Ref 세계'의 일관된 스냅샷을 볼 거예요. 트랜잭션 자기가 만든 변경은 볼 거예요. 이것을 인-트랜잭션 값(in-transaction-value) 이라고 불러요. . 트랜잭션 동안 Ref에 대한 모든 변경(ref-set, alter 또는 commute을 통해)은 'Ref 세계' 타임라인의 한 지점(그 '쓰기 지점')에서 발생한 것으로 보일 거예요. . 이 트랜잭션이 ref-set/alter/_ensure_한 Ref들에는 다른 트랜잭션에 의한 변경이 없을 거예요. . 이 트랜잭션이 commute한 Ref들에는 다른 트랜잭션에 의한 변경이 있을 수도 있어요. _commute_가 적용하는 함수는 교환적(commutative)이어야 하므로 그건 괜찮아야 해요. . 독자(readers)와 커뮤터(commuters)는 쓰기자, 커뮤터, 또는 다른 독자를 절대 블록하지 않아요. . 쓰기자는 커뮤터나 독자를 절대 블록하지 않아요. . I/O와 그 밖의 부수 효과가 있는 활동은 트랜잭션에서 피해야 해요 — 트랜잭션 재시도되니까요. io! 매크로를 사용해 트랜잭션에서 불순(impure) 함수의 사용을 막을 수 있어요. . 변경 중인 Ref 값의 유효성 제약이 변경되지 않는 Ref의 동시 값에 의존한다면, 그 두 번째 Ref는 ensure를 호출해 수정으로부터 보호할 수 있어요. 이렇게 'ensured'된 Ref는 보호되고(3번 항목), 세계를 바꾸지 않아요(2번 항목). . Clojure MVCC STM은 영속 컬렉션과 함께 동작하도록 설계됐고, Ref의 값으로 Clojure 컬렉션을 사용하는 것을 강력히 권장해요. STM 트랜잭션에서 하는 모든 작업은 추측적(speculative)이므로, 복사와 수정의 비용이 낮아야 하는 것이 필수예요. 영속 컬렉션은 복사가 무료이고(원본을 그냥 쓰면 됨 — 바뀔 수 없으니까), '수정'은 구조를 효율적으로 공유해요. 어쨌든: . Ref에 넣는 값은 반드시 불변이거나 불변으로 간주되어야 해요!! 그렇지 않으면 Clojure가 도와줄 수 없어요.

출처: Clojure 공식 문서 - Refs and Transactions

본문

예시 (Example)

이 예시에서는 벡터들의 ref 벡터를 만드는데, 각각 (처음에는 순차적인) 고유 숫자들을 담아요. 그런 다음 두 개의 임의 벡터에서 임의 위치 둘을 반복적으로 골라 트랜잭션 안에서 맞바꾸는 스레드 집합을 시작해요. 트랜잭션을 사용하는 것 외에는 피할 수 없는 충돌을 막기 위한 특별한 노력을 하지 않아요.

(defn run [nvecs nitems nthreads niters]
  (let [vec-refs (vec (map (comp ref vec)
                           (partition nitems (range (* nvecs nitems)))))
        swap #(let [v1 (rand-int nvecs)
                    v2 (rand-int nvecs)
                    i1 (rand-int nitems)
                    i2 (rand-int nitems)]
                (dosync
                 (let [temp (nth @(vec-refs v1) i1)]
                   (alter (vec-refs v1) assoc i1 (nth @(vec-refs v2) i2))
                   (alter (vec-refs v2) assoc i2 temp))))
        report #(do
                 (prn (map deref vec-refs))
                 (println "Distinct:"
                          (count (distinct (apply concat (map deref vec-refs))))))]
    (report)
    (dorun (apply pcalls (repeat nthreads #(dotimes [_ niters] (swap)))))
    (report)))

실행하면 셔플에서 값이 유실되거나 중복되지 않는 것을 볼 수 있어요:

(run 100 10 10 100000)

([0 1 2 3 4 5 6 7 8 9] [10 11 12 13 14 15 16 17 18 19] ...
 [990 991 992 993 994 995 996 997 998 999])
Distinct: 1000

([382 318 466 963 619 22 21 273 45 596] [808 639 804 471 394 904 952 75 289 778] ...
 [484 216 622 139 651 592 379 228 242 355])
Distinct: 1000

더 알아보기