가드와 락

가드와 락 (Guards and locks)

Nim은 락(lock), 원자적 내장 함수(atomic intrinsic), 조건 변수(condition variable) 같은 흔한 저수준 동시성 메커니즘을 제공해요.

Nim은 추가 프라그마를 통해 이런 기능들의 안전성을 크게 높여줘요:

  1. 데이터 레이스(data race)를 막기 위해 가드(guard):idx: 어노테이션을 도입했어요.
  2. 가드된 메모리 위치에 접근할 때마다 반드시 적절한 locks:idx: 문 안에서 이뤄져야 해요.

가드와 락 섹션 (Guards and locks sections)

전역 변수 보호하기 (Protecting global variables)

객체 필드와 전역 변수는 guard 프라그마로 어노테이션할 수 있어요:

import std/locks

var glock: Lock
var gdata {.guard: glock.}: int

그러면 컴파일러는 gdata에 대한 모든 접근이 locks 섹션 안에서 일어나도록 강제해요:

proc invalid =
  # 유효하지 않음: 가드되지 않은 접근:
  echo gdata

proc valid =
  # 유효한 접근:
  {.locks: [glock].}:
    echo gdata

gdata에 대한 최상위(top level) 접근은 항상 허용돼서 편리하게 초기화할 수 있어요. 모든 최상위 문장은 어떤 동시 작업이 일어나기 전에 실행된다고 가정하지만(그리고 강제하지는 않아요) 그래요.

locks 섹션은 일부러 보기 흉하게 만들었어요. 런타임 의미(runtime semantics)가 없어서 직접 쓰면 안 되거든요! 런타임에 어떤 형태의 락킹을 함께 구현하는 템플릿 안에서만 쓰도록 하세요:

template lock(a: Lock; body: untyped) =
  pthread_mutex_lock(a)
  {.locks: [a].}:
    try:
      body
    finally:
      pthread_mutex_unlock(a)

가드는 특정 타입일 필요가 없어요. 저수준 락프리(lockfree) 메커니즘을 모델링할 만큼 유연해요:

var dummyLock {.compileTime.}: int
var atomicCounter {.guard: dummyLock.}: int

template atomicRead(x): untyped =
  {.locks: [dummyLock].}:
    memoryReadBarrier()
    x

echo atomicRead(atomicCounter)

locks 프라그마는 멀티 락(multi lock) 문을 지원하기 위해 락 표현식의 목록 locks: [a, b, ...]을 받아요.

일반적인 위치 보호하기 (Protecting general locations)

guard 어노테이션은 객체 안의 필드를 보호할 때도 쓸 수 있어요. 이때 가드는 같은 객체 안의 다른 필드이거나 전역 변수여야 해요.

객체는 힙(heap)이나 스택(stack)에 존재할 수 있으므로, 이 기능은 언어의 표현력을 크게 높여줘요:

import std/locks

type
  ProtectedCounter = object
    v {.guard: L.}: int
    L: Lock

proc incCounters(counters: var openArray[ProtectedCounter]) =
  for i in 0..counters.high:
    lock counters[i].L:
      inc counters[i].v

필드 x.v에 대한 접근은 그 가드인 x.L이 활성화되어 있으므로 허용돼요. 템플릿 확장 후에는 이렇게 됩니다:

proc incCounters(counters: var openArray[ProtectedCounter]) =
  for i in 0..counters.high:
    pthread_mutex_lock(counters[i].L)
    {.locks: [counters[i].L].}:
      try:
        inc counters[i].v
      finally:
        pthread_mutex_unlock(counters[i].L)

counters[i].L이 보호되는 위치인 counters[i].v에 대응하는 락인지 검사하는 분석이 있어요. 이 분석은 obj.field[i].fieldB[j] 같은 위치에 대한 경로(path)를 다루기 때문에 경로 분석(path analysis):idx: 이라고 불러요.

경로 분석은 현재는 부정확(unsound)하지만, 그렇다고 쓸모없어지지는 않아요. 두 경로는 구문적으로 동일하면 동등한 것으로 간주돼요.

따라서 다음 코드는 정말 그래서는 안 되지만 (지금은) 컴파일돼요:

{.locks: [a[i].L].}:
  inc i
  access a[i].v