액션 속성
액션 속성
지난 장에서 모든 불변식(invariant)은 안전성 속성(safety property)이지만 모든 안전성 속성이 불변식인 것은 아니라고 했다. 불변식을 빼면 가장 큰 부류의 안전성 속성은 “액션 속성(action property)”으로, 시스템이 어떻게 변할 수 있는지에 대한 제약이다.
threads 스펙(spec)을 조금 더 가지고 놀아 보자.
---- MODULE threads ----
EXTENDS TLC, Integers
CONSTANT NULL
NumThreads == 2
Threads == 1..NumThreads
(* --algorithm threads
variables
counter = 0;
lock = NULL;
define
end define;
process thread \in Threads
variables tmp = 0;
begin
GetLock:
await lock = NULL;
lock := self;
GetCounter:
tmp := counter;
IncCounter:
counter := tmp + 1;
ReleaseLock:
lock := NULL;
end process;
end algorithm; *)
====
spec이 스펙이 변할 수 있는 방식에 걸 만한 제약을 두어 가지 들어 보면 다음과 같다.
counter는 증가하기만 해야 한다.어떤 스레드(thread)가 락을 쥐고 있다면, 락이 먼저 해제되지 않고는 다른 스레드로 넘어갈 수 없다.
첫 번째 제약을 액션 속성으로 쓰면 다음과 같다.
lock = NULL;
define
+ CounterOnlyIncreases ==
+ [][counter' >= counter]_counter
end define;
process thread \in Threads
잠깐, 뭐라고?
걱정 마라. 이 문법은 다음 절에서 설명하겠다. 지금은 이게 실제로 깨지는지부터 확인해 보자. 먼저 counter가 감소하도록 바꿔 보자.
tmp := counter;
IncCounter:
- counter := tmp + 1;
+ counter := tmp + IF tmp = 0 THEN 1 ELSE -1;
ReleaseLock:
lock := NULL;
spec이제 이것을 PROPERTY CounterOnlyIncreases로 실행하자(불변식으로 넣는 게 아니다). 제대로 설정했다면 다음 에러가 보일 것이다.
\* some initial states
State 7:
/\ counter = 1
/\ lock = 1
/\ pc = <<"IncCounter", "Done">>
/\ tmp = <<1, 0>>
State 8:
/\ counter = 0
/\ lock = 1
/\ pc = <<"ReleaseLock", "Done">>
/\ tmp = <<1, 0>>
이것이 실패하는 이유는 counter가 0인 상태가 있어서가 아니다. 그 상태는 이 스펙에서 완전히 유효한 상태이고, 사실 시작 상태이기까지 하다! 실패하는 이유는 counter가 1에서 0으로 바뀌기 때문이다. 에러는 counter가 감소한다는 사실 그 자체다.
![digraph G {
label="val: counter";
0 -> 1 -> 2;
1 -> 0[color=tomato];
}](/tla/_images/graphviz-b3a7f04c54633b4128ec49509725338f7ecef62a.png)
이 상태들 중 불법인 것은 하나도 없지만, 전이(transition), 즉 counter=1에서 counter=0으로 넘어가는 것은 불법이다.
문법 이해하기
한편으로는 근사한 묘기다. 다른 한편으로는, 이제 [][counter' >= counter]_counter가 대체 무슨 뜻인지 알아내야 한다.
드디어 “액션의 시간 논리(Temporal Logic of Actions)”에서 말하는 “액션”에 대해 이야기할 때가 왔다.
한참 전에 문자열에는 반드시 큰따옴표를 써야 한다고 했던 것을 기억하는가? 작은따옴표가 TLA+에서 특별한 역할을 맡고 있기 때문이다. 어떤 스텝(step)에서든 x'는 스텝이 끝날 때의 x 값이자 다음 스텝에서 x가 시작하는 값이다. 그러니 [](x' >= x)는 “x의 다음 값이 x보다 크거나 같다는 것은 항상 참이다”라는 뜻이다.
팁
트레이스 탐색기(Trace Explorer)에서도 프라임이 붙은 연산자(operator)를 쓸 수 있다. 그러면 다음 스텝에서 그 식의 값을 보여 준다.
하지만 이건 (아직) 유효한 TLA+ 속성이 아니다. 살짝 다른 속성 [](x' = x + 1), 즉 “x는 항상 정확히 1씩 증가한다”를 생각해 보자. 여기에 스터터 스텝(stutter step)을 끼워 넣으면 어떻게 될까? 그러면 x가 전혀 바뀌지 않으니 이 속성은 거짓이 된다. 그런데 TLA+의 정의상 스터터 스텝은 언제든 어디에나 끼워 넣을 수 있다. 그러므로 이 속성은 자명하게 거짓이다. 우리가 실제로 검사하고 싶었던 더 흥미로운 속성은 [](x' # x => x' + 1)이었다. 아니면 이를 x' > x \/ UNCHANGED x로 쓸 수도 있다.
여기에 문법 설탕을 하나 더 얹으면, [](x' = x + 1 \/ UNCHANGED x)를 [][x' = x + 1]_x로 쓸 수 있다. 이것을 박스 액션 공식(box action formula)이라고 부른다. 다음 장에서 보겠지만 박스 액션 공식은 TLA+에서 특별한 역할을 한다. TLC는 박스 액션 공식 형태의 액션 속성만 검사할 수 있다.
팁
밑줄스러운 부분(_)이 있다는 건, 이 속성을 [][counter' > counter]_counter로 쓸 수도 있었다는 뜻이다. 모든 단계를 전개해 보면 다음과 같다.
[counter' > counter]_countercounter' > counter \/ UNCHANGED countercounter' > counter \/ counter' = countercounter' >= counter
하지만 일반적으로는 속성을 쓸 때 []_x의 이런 면에 기대지 않는 게 좋다. counter가 그대로 있어도 괜찮다면 그 점을 명시적으로 드러내라.
액션 속성 더 알아보기
“락은 한 스레드에서 다른 스레드로 곧바로 넘어갈 수 없다”는 속성을 하나 더 추가해 보자.
define
CounterOnlyIncreases ==
[][counter' >= counter]_counter
+
+ LockCantBeStolen ==
+ [][lock # NULL => lock' = NULL]_lock
end define;
process thread \in Threads
@@ -27,7 +30,7 @@
tmp := counter;
IncCounter:
- counter := tmp + IF tmp = 0 THEN 1 ELSE -1;
+ counter := tmp + 1;
ReleaseLock:
lock := NULL;
그리고 이제 이 속성을 깨뜨리는 변경을 가해 보자.
variables tmp = 0;
begin
GetLock:
- await lock = NULL;
lock := self;
GetCounter:
PROPERTY LockCantBeStolen으로 실행하면 이것이 실패하는 것을 볼 수 있다.
![digraph LockCantBeStolen {
rankdir=TB;
label="val: lock";
NULL -> {t1 t2};
{t1 t2} -> NULL;
t1 -> t2[color=tomato];
}](/tla/_images/graphviz-d3ec244dfbe6ab5bdd9b36644b83c92ab1d933a7.png)
이 속성을 이렇게 쓸 수도 있었다.
LockCantBeStolen ==
[][lock # NULL => lock' = NULL]_lock
+
+ LockNullBeforeAcquired ==
+ [][lock' # NULL => lock = NULL]_lock
end define;
process thread \in Threads
액션 속성에서 헬퍼 액션(helper action)을 쓸 수 있으므로, 이런 식으로도 할 수 있다.
BecomesNull(x) == x' = NULL
LockCantBeStolen ==
[][lock # NULL => BecomesNull(lock)]_lock
한정자를 쓰는 액션 속성
앞에서 TLC는 최상위 액션 속성만 검사할 수 있다고 했다. 이 때문에 어떤 일은 조금 번거로워진다. 독립적인 카운터가 여러 개 있는 간단한 스펙을 하나 써 보자.
---- MODULE counters ----
EXTENDS Integers
Counters == {1, 2}
(* --algorithm counters
variables
values = [i \in Counters |-> 0];
define
end define;
macro increment() begin
values[self] := values[self] + 1;
end macro
process counter \in Counters
begin
A:
increment();
B:
increment();
end process;
end algorithm; *)
=====
spec앞에서처럼 카운터들이 단조적이라는 액션 속성을 원한다. 앞에서와 달리 이번에는 한정자(quantifier)로 묶어야 할 카운터가 여러 개다.
values = [i \in Counters |-> 0];
define
+ CounterOnlyIncreases ==
+ \A c \in Counters:
+ [][values[c]' >= values[c]]_values[c]
end define;
macro increment() begin
spec안타깝게도 모델 체커(model checker)의 한계 때문에 TLC는 이것을 검사하지 못한다.
[] followed by action not of form [A]_v.
(에러 메시지가 조금 헷갈리지만, 액션 속성을 한정자 안에 넣으면 언제나 이 에러가 난다.)
이럴 때는 한정자를 액션 속성 안으로 끌어들이면 된다. 알고 보니 []는 \A와 교환 가능하다! 다시 말해 \A x: []P(x)로 쓴 식은 어느 것이든 다음 식과 동치다: [](\A x: P(x)).
define
CounterOnlyIncreases ==
+ [][
\A c \in Counters:
- [][values[c]' >= values[c]]_values[c]
+ values[c]' >= values[c]
+ ]_values
end define;
macro increment() begin
액션 속성 활용하기
내가 쓰는 스펙 대부분은 액션 속성보다 불변식이 더 많고, 라이브니스 속성(liveness property)보다 액션 속성이 더 많다. 하지만 모든 스펙에는 라이브니스 속성이 적어도 하나는 필요하므로, 라이브니스 속성이 액션 속성보다 더 “중요”하다고 볼 수도 있다. 액션 속성은 강력하지만 선택 사항이다.
그렇긴 해도 나는 액션 속성을 쓰는 게 정말 좋다. 액션 속성은 새로운 속성을 정의하는 데 엄청난 유연성을 준다.
요약
액션 속성은 시스템의 전이에 대한 속성이며, 시간 속성(temporal property)으로 검사된다.
x'는x가 다음 상태에서 갖는 값이다. 프라임이 들어 있는 연산자를 액션이라고 부른다.[P]_x는P \/ UNCHANGED x를 뜻한다.