원자성 위반

원자성 위반이라는 동시성 오류 유형이 있다. 이 유형의 오류는 정확한 동기화 없이 객체의 상태를 동시에 수정할 때 발생한다. ex) 서로 다른 스레드에 있는 여러 코루틴이 객체의 상태를 수정하면 일부 수정 사항이 유실됨.

원자성 위반은 코틀린에서도 발생할 수 있지만 오류를 피할 수 있도록 디자인하는 데 도움이 되는 기본형(primitives)을 제공한다.

원자성의 의미

원자적 : 수행도중 중단될 수 없는 하나의 동작단위. 모두 성공하거나, 실패하거나

소프트웨어 실행의 관점에서, 연산(operation)이 단일하고 분할할 수 없을 때 이 연산을 원자적(atomic)이라 한다. 공유 상태에 관해 언급할 때 흔히 많은 스레드에서 하나의 변수를 읽거나 쓰는 것에 대해 이야기한다.

변수의 상태를 수정하는 것은 일반적으로 변수 값을 읽고 수정하고 업데이트된 값을 저장하는 것처럼 여러 단계로 구성돼 있다. 보통 이렇게 원자적이지 않아서 문제가 발생한다.

동시성 애플리케이션을 실행하게 되면 공유 상태를 수정하는 코드 블록이 다른 스레드의 변경 시도와 겹치면서 이런 문제가 발생한다. 한 스레드가 현재 값을 바꾸는 중에 아직 쓰지는 않은 상태에서 다른 스레드가 현재 값을 읽을 수 있다. 이런 상황은 하나 또는 그 이상의 공유 상태에 대한 변경사항이 덮어 씌워져 유실될 수 있음을 의미한다.

private var counter = 0

fun increment() {
		counter++
}

순차적으로 실행하면, counter 값에 대해 걱정할 필요 없이 increment()를 원하는 만큼 호출할 수 있다. counter의 값은 항상 increment()가 호출된 횟수와 일치한다.

그러나 여기에 동시성을 추가하면 내부의 많은 것들이 바뀐다.

var counter = 0

fun asyncIncrement(by: Int) = async(CommonPool[1]){
		for(i in 0 until by) {
				counter++
		}
}

CommonPool을 CoroutineContext로 사용해 요청한 횟수만큼 counter를 늘리고 있다. 하나 이상의 프로세스 유닛이 있는 디바이스에서 실행한다고 가정하고 메인 함수에서 호출할 수 있다.

val workerA = asyncIncrement(2000)
val workerB = asyncIncrement(100)

workerA.await()
workerB.await()

Log.d("counter : ", counter.toString())

실행 후 counter의 값이 가끔 2100보다 낮다는 것을 알 수 있다.

counter++를 수행하는 코드가 원자적이지 않아서 발생한다. 이 한줄의 코드는 읽기, 수정 및 쓰기의 세 가지 작업으로 나눌 수 있으며, 스레드가 작동하는 방식 때문에 한 스레드의 쓰기 변경사항이 다른 스레드에서 값을 읽거나 수정할 때 보이지 않을 수 있다. 여러 스레드가 잠재적으로 counter를 같은 값으로 증가시킬수 있다.

이것이 실제로 의미하는 바는 asyncIncrement 내부의 여러 for 루프 중 여러 사이클이 counter값을 오직 한 번만 바꿨다는 것이다.

코드 블록을 원자적으로 만들려면 블록 안에서 발생하는 어떤 메모리 엑세스도 동시에 실행되지 않도록 해야 한다. 여러 가지 방법으로 수행할 수 있으며 가장 좋은 방법은 상황의 특성에 따라 다르다.

스레드 한정