raku-type-atomicint

class atomicint is Int is repr('P6int') { }

atomicintCPU가 제공하는 원자 연산(atomic operation)을 수행할 수 있는 크기의 네이티브 정수예요. 32비트 CPU에서는 보통 32비트, 64비트 CPU에서는 보통 64비트 크기죠. 원자 연산을 쓰는 이식성 있는 코드를 작성하기 위해 존재하는 타입이에요.

참고: 2017.08 이전 Rakudo 릴리스에는 atomicint 지원이 없었어요.

# Would typically only work on a 64-bit machine and VM build.
my int64 $active = 0;
$active⚛++;

# Would typically only work on a 32-bit machine and VM build.
my int32 $active = 0;
$active⚛++;

# Will work portably, though can only portably assume range of 32 bits.
my atomicint $active = 0;
$active⚛++;

한 가지 짚고 넘어갈 점은, atomicint 타입을 쓴다고 자동으로 원자성이 보장되는 건 아니라는 거예요. 원자 연산과 함께 사용해야 해요.

# Correct (will always output 80000)
my atomicint $total = 0;
await start { for ^20000 { $total⚛++ } } xx 4;
say $total;

# *** WRONG *** due to lack of use of the atomicint type.
# Either works correctly or dies, depending on platform.
my int $total = 0;
await start { for ^20000 { $total⚛++ } } xx 4;
say $total;

# *** WRONG *** due to lack of use of the atomic increment operator.
my atomicint $total = 0;
await start { for ^20000 { $total++ } } xx 4;
say $total;

Routines

sub atomic-assign

multi atomic-assign(atomicint $ is rw, int $value)
multi atomic-assign(atomicint $ is rw, Int() $value)

네이티브 정수에 원자적 할당을 수행해요. 대상은 레시컬, 속성, 또는 네이티브 배열 원소 어디에 있어도 돼요. $value가 너무 커서 64비트 네이티브 정수로 언박싱할 수 없으면 예외가 던져져요. atomicint 크기가 32비트뿐이라면 범위를 벗어난 $value는 조용히 잘려요. atomic-assign은 필요한 배리어(barrier)를 모두 수행해서, 바뀐 값이 다른 스레드에 "공개"되도록 보장해요.

sub atomic-fetch

multi atomic-fetch(atomicint $ is rw)

네이티브 정수의 원자적 읽기를 수행해요. 대상은 레시컬, 속성, 또는 네이티브 배열 원소 어디에 있어도 돼요. 그냥 변수를 읽는 대신 이 루틴을 쓰면, 필요한 하드웨어 배리어를 수행하고 컴파일러가 읽기를 들어 올리는 것을 막아서, 다른 스레드의 최신 업데이트를 확실히 보게 돼요. 예를 들어

my atomicint $i = 0;
start { atomic-assign($i, 1) }
while atomic-fetch($i) == 0 { }

이 코드는 반드시 끝나는 게 보장돼요. 반면에

my atomicint $i = 0;
start { atomic-assign($i, 1) }
while $i == 0 { }

이렇게 쓰면, 컴파일러가 루프 안에서 $i가 갱신되지 않음을 관찰하고 읽기를 루프 밖으로 들어 올려서 프로그램이 끝나지 않을 수도 있어요. (그렇게 해도 법적으로는 문제없어요.)

sub atomic-fetch-inc

multi atomic-fetch-inc(atomicint $ is rw)

네이티브 정수에 원자적 증가를 수행해요. 하드웨어가 제공하는 원자 연산으로 처리돼요. 원자적이므로 락을 걸지 않아도 안전하게 쓸 수 있어요. 증가하기 전의 값을 돌려줘요. 오버플로는 조용히 감겨요(wrap).

sub atomic-fetch-dec

multi atomic-fetch-dec(atomicint $ is rw)

네이티브 정수에 원자적 감소를 수행해요. 하드웨어가 제공하는 원자 연산으로 처리되며, 락 없이 안전해요. 감소하기 전의 값을 돌려줘요. 오버플로는 조용히 감겨요.

sub atomic-fetch-add

multi atomic-fetch-add(atomicint $ is rw, int $value)
multi atomic-fetch-add(atomicint $ is rw, Int() $value)

네이티브 정수에 원자적 덧셈을 수행해요. 하드웨어 원자 연산으로 처리되고, 락 없이 안전해요. 덧셈을 수행하기 전의 값을 돌려줘요. 오버플로는 조용히 감겨요. $value가 너무 커서 64비트 정수로 언박싱할 수 없으면 예외가 나요. 아니라면 덧셈을 수행하기 전에 atomicint를 넘치는 값은 조용히 잘려요.

sub atomic-fetch-sub

multi atomic-fetch-sub(atomicint $ is rw, int $value)
multi atomic-fetch-sub(atomicint $ is rw, Int() $value)

네이티브 정수에 원자적 뺄셈을 수행해요. 하드웨어 원자 연산으로 처리되고, 락 없이 안전해요. 뺄셈을 수행하기 전의 값을 돌려줘요. 언더플로는 조용히 감겨요. $value가 너무 커서 64비트 정수로 언박싱할 수 없으면 예외가 나요. 아니라면 뺄셈을 수행하기 전에 atomicint를 넘치는 값은 조용히 잘려요.

sub atomic-inc-fetch

multi atomic-inc-fetch(atomicint $ is rw)

네이티브 정수에 원자적 증가를 수행해요. 하드웨어 원자 연산으로 처리되고, 락 없이 안전해요. 증가로 인한 결과 값을 돌려줘요. 오버플로는 조용히 감겨요.

sub atomic-dec-fetch

multi atomic-dec-fetch(atomicint $ is rw)

네이티브 정수에 원자적 감소를 수행해요. 하드웨어 원자 연산으로 처리되고, 락 없이 안전해요. 감소로 인한 결과 값을 돌려줘요. 오버플로는 조용히 감겨요.

sub cas

multi cas(atomicint $target is rw, int $expected, int $value)
multi cas(atomicint $target is rw, Int() $expected, Int() $value)
multi cas(atomicint $target is rw, &operation)

$target 위치의 네이티브 정수 값에 **원자적 비교 후 교환(compare and swap)**을 수행해요. 앞의 두 형식은 대략 이런 의미를 가져요.

my int $seen = $target;
if $seen == $expected {
    $target = $value;
}
return $seen;

다만 이 작업이 실행되는 동안 $target에 대한 모든 메모리 접근이 막힌 것처럼, 단일 하드웨어 원자 명령으로 수행된다는 점이 달라요. 그래서 다른 동기화 없이 여러 스레드에서 이 연산을 시도해도 안전해요. 예를 들어

my atomicint $master = 0;
await start {
    if cas($master, 0, 1) == 0 {
        say "Master!"
    }
} xx 4

이 코드는 신뢰성 있게 Master!딱 한 번만 출력해요. 0을 1로 바꾸는 데 성공하는 스레드는 오직 하나뿐이기 때문이죠.

$expected$value는 모두 필요하면 Int로 강제 변환되고 언박싱돼요. 값을 64비트 정수로 나타낼 수 없으면 예외가 나요. atomicint 크기가 32비트뿐이면 값들은 이 크기로 조용히 잘려요.

코드 객체를 받는 세 번째 형식을 볼게요. 먼저 현재 값을 원자적으로 fetch한 뒤 그 값으로 코드 객체를 호출해요. 그다음 코드 객체에 넘긴 값을 $expected로, 코드 객체의 결과를 $value로 하여 대상의 원자적 비교 후 교환을 시도해요. 실패하면 최신 값을 읽고, CAS가 성공할 때까지 다시 시도해요. 그래서 atomicint $i를 2로 곱하는 원자적 곱셈은 이렇게 구현할 수 있어요.

cas $i, -> int $current { $current * 2 }

$current * 2를 계산하는 동안 다른 스레드가 값을 바꿨다면, 블록이 최신 값으로 다시 호출되어 또 시도하고, 성공할 때까지 반복돼요.

Operators

infix ⚛=

multi infix:<⚛=>(atomicint $ is rw, int $value)
multi infix:<⚛=>(atomicint $ is rw, Int() $value)

네이티브 정수에 원자적 할당을 수행해요. 대상은 레시컬, 속성, 또는 네이티브 배열 원소 어디에 있어도 돼요. $value가 너무 커서 64비트 네이티브 정수로 언박싱할 수 없으면 예외가 나요. atomicint 크기가 32비트뿐이면 범위를 벗어난 $value는 조용히 잘려요. ⚛= 연산자는 필요한 배리어를 모두 수행해서, 바뀐 값이 다른 스레드에 "공개"되도록 보장해요.

prefix ⚛

multi prefix:<⚛>(atomicint $ is rw)

네이티브 정수의 원자적 읽기를 수행해요. 대상은 레시컬, 속성, 또는 네이티브 배열 원소 어디에 있어도 돼요. 그냥 변수를 읽는 대신 이 연산자를 쓰면, 필요한 하드웨어 배리어를 수행하고 컴파일러가 읽기를 들어 올리는 것을 막아서, 다른 스레드의 최신 업데이트를 확실히 보게 돼요. 예를 들어

my atomicint $i = 0;
start { $i ⚛= 1 }
while ⚛$i == 0 { }

이 코드는 반드시 끝나요. 반면에

my atomicint $i = 0;
start { $i ⚛= 1 }
while $i == 0 { }

이렇게 쓰면 컴파일러가 루프 안에서 $i가 갱신되지 않음을 관찰해 읽기를 루프 밖으로 들어 올려서, 프로그램이 끝나지 않을 수도 있어요.

prefix ++⚛

multi prefix:<++⚛>(atomicint $ is rw)

네이티브 정수에 원자적 증가를 수행해요. 하드웨어 원자 연산으로 처리되고, 락 없이 안전해요. 증가로 인한 결과 값을 돌려줘요. 오버플로는 조용히 감겨요.

postfix ⚛++

multi postfix:<⚛++>(atomicint $ is rw)

네이티브 정수에 원자적 증가를 수행해요. 하드웨어 원자 연산으로 처리되고, 락 없이 안전해요. 증가하기 전의 값을 돌려줘요. 오버플로는 조용히 감겨요.

prefix --⚛

multi prefix:<--⚛>(atomicint $ is rw)

네이티브 정수에 원자적 감소를 수행해요. 하드웨어 원자 연산으로 처리되고, 락 없이 안전해요. 감소로 인한 결과 값을 돌려줘요. 오버플로는 조용히 감겨요.

postfix ⚛--

multi postfix:<⚛-->(atomicint $ is rw)

네이티브 정수에 원자적 감소를 수행해요. 하드웨어 원자 연산으로 처리되고, 락 없이 안전해요. 감소하기 전의 값을 돌려줘요. 오버플로는 조용히 감겨요.

infix ⚛+=

multi infix:<⚛+=>(atomicint $ is rw, int $value)
multi infix:<⚛+=>(atomicint $ is rw, Int() $value)

네이티브 정수에 원자적 덧셈을 수행해요. 하드웨어 원자 연산으로 처리되고, 락 없이 안전해요. 덧셈의 결과로 평가돼요. 오버플로는 조용히 감겨요. $value가 너무 커서 64비트 정수로 언박싱할 수 없으면 예외가 나요. 아니라면 덧셈을 수행하기 전에 atomicint를 넘치는 값은 조용히 잘려요.

infix ⚛-=

multi infix:<⚛-=>(atomicint $ is rw, int $value)
multi infix:<⚛-=>(atomicint $ is rw, Int() $value)

네이티브 정수에 원자적 뺄셈을 수행해요. 하드웨어 원자 연산으로 처리되고, 락 없이 안전해요. 뺄셈의 결과로 평가돼요. 언더플로는 조용히 감겨요. $value가 너무 커서 64비트 정수로 언박싱할 수 없으면 예외가 나요. 아니라면 뺄셈을 수행하기 전에 atomicint를 넘치는 값은 조용히 잘려요.

infix ⚛−=

U+2212 마이너스 부호를 쓰는 ⚛-=의 동의어예요.