atomic/non atomic
01 Mar 2019atomic
은 ObjectiveC를 경험하였던 분들에게는 익숙한 개념이지만, Swift만 사용했던 분들에게는 익숙하지 않을 수 있는 개념입니다. atomic
의 정의에 대해서 살펴보면 다음과 같습니다.
- atomic - 중단되지 않는
an operation appears to occur at a single instant between its invocation and its response
atomic
하다는 것은 프로그래밍에서 데이터의 변경이 한 번에 일어난 것처럼 보이게 하는 것을 의미합니다. 데이터의 값을 변경하는 작업에는 항상 값 변경의 시간이 필요합니다. 그런데 atomic
한 데이터는 데이터의 값에 접근하는 여러 데이터 소비자(프로세스, 쓰레드 등)의 관점에서 데이터 값 변경에 걸리는 시간이 0초인 것처럼 느끼게 합니다. 이게 어떻게 가능할까요? ObjectiveC에서는 atomic
한 데이터를 set할 때 lock을 걸어서 atomic
데이터를 만들 수 있도록 해줍니다.
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spin_lock_t *slotlock = &PropertyLocks[GOODHASH(slot)];
_spin_lock(slotlock);
oldValue = *slot;
*slot = newValue;
_spin_unlock(slotlock);
}
쓰레드에 lock을 건다는 것은 데이터를 변경할 때, 데이터 변경 작업 이외의 다른 모든 작업이 멈추도록 만듭니다.(좀 더 정확히는 SpinLock을 걸게 되면, 크리티컬 섹션의 동작이 모두 끝날 때까지 쓰레드가 루프를 돌면서 busy waiting하게 됩니다.) 그래서 이는 달리 말하면, 데이터가 시간 소모 없이 바로 변경된 것처럼 보이게 합니다. 데이터 변경시에 데이터 업데이트 이외에는 아무런 작업이 일어나지 않았기 때문입니다. 이런 방식으로 atomic
데이터는 멀티 쓰레드 환경에서 데이터가 반드시 변경 전, 변경 후의 상황에서만 접근되도록 하는 것을 보장합니다. 즉, 데이터의 변경 중에는 해당 데이터에 접근이 불가능(lock이 걸려 있으므로)합니다.
- Lock에 대한 좀 더 자세한 설명은 Atomic Properties in Swift을 참고하면 좋습니다.
atomic과 ObjectiveC
ObjectiveC Property는 기본적으로 atomic
으로 선언됩니다. 다만, atomic
property는 위에서 본 것처럼 atomic
데이터가 변경 도중에 접근하지 못 하도록 lock이 걸리기 때문에 property 접근의 성능이 느려집니다. 그래서 멀티 쓰레드에서 접근될 이유가 없는 많은 ObjectiveC Property(반드시 메인스레드에서 업데이트 해야하는 View Property 같은 것들)에는 nonatomic
annotation을 설정하는 것이 좋습니다.
@interface Asset : NSObject
// name을 nonatomic으로 사용
@property (nonatomic, strong) NSString *name;
@end
atomic과 Swift
한편, Swift는 Thread-Safe를 고려하고 디자인된 언어가 아니기 때문에 모든 property는 non atomic
입니다. 그리고 별도로 atomic
옵션을 지정할 수도 없습니다. 그래서 Swift의 property가 atomic
을 지원하기 위해서는 GCD를 통해 이를 구현해주어야 합니다. 다음 글에서 자세한 내용을 확인할 수 있습니다. 여기에서는 위 글에서 작성된 것을 확인하는 예제만 간단히 남기도록 하겠습니다.
class AtomicValue<T> {
let queue = DispatchQueue(label: "queue")
private(set) var storedValue: T
init(_ storedValue: T) {
self.storedValue = storedValue
}
var value: T {
get {
return queue.sync {
self.storedValue
}
}
set { // read, write 접근 자체는 atomic하지만,
// 다른 쓰레드에서 데이터 변경 도중(read, write 사이)에 접근이 가능하여, 완벽한 atomic이 아닙니다.
queue.sync {
self.storedValue = newValue
}
}
}
// 올바른 방법
func mutate(_ transform: (inout T) -> ()) {
queue.sync {
transform(&self.storedValue)
}
}
}
let atomicInComplete = AtomicValue<Int>(0)
let atomicComplete = AtomicValue<Int>(0)
DispatchQueue.concurrentPerform(iterations: 100) { (idx) in
atomicInComplete.value += idx
atomicComplete.mutate { $0 += idx }
}
print(atomicInComplete.storedValue) // 결과: 돌릴 때마다 다름
print(atomicComplete.storedValue) // 결과: 4950