← go back

Concurrency


소스코드의 특정 블럭을 동기화시키고자 할 때, 항상 메모리 가시성(Memory Visibility) 문제가 발생한다. 특정 변수의 값을 사용하고 있을 때 다른 스레드가 그 변수의 값에 접근하지 못하도록 막아야 하는 상호배제(Mutual exclusion) 도 중요하지만 값을 수정한 다음 동기화 블록을 빠져나가고 나면 다른 스레드가 변경된 값을 즉시 사용할 수 있어야 하는 가시성(Visibility) 도 중요하다.

가시성(Visibility)

싱글 스레드 환경에서는 프로그램의 코드가 특정 변수에 값을 수정한 다음 다시 그 변수의 값에 접근해보면 이전에 수정한 값을 다시 가져올 수 있다. 멀티 스레드 환경에서는 반드시 수정한 값을 읽는 것이 보장되지는 않는다. 공유 변수에 대해서 어떤 스레드가 값을 수정했을 때, 그 값을 다른 스레드가 읽어갈 수 있다는 보장이 없다. 수정하기 전 변수 값을 읽거나 심지어 값을 읽어가지 못할 수도 있다.

멀티프로세서에서 Stale Data 현상

Thread는 동작하는 시점에 하나의 CPU Core를 점유하고 동작을 한다. 선언한 변수의 값이 Memory에만 존재하는 것이 아니라 CPU Cahce라고 하는 영역에도 존재한다. 이는 CPU가 Memory에서 값을 읽어들여오고 다시 쓰고 하는 시간을 아끼기 위함이다. 더 큰 문제는 CPU Cache에 값이 Memory에 언제 옮겨갈지도 모른다는 것이다. 이를 해결하는 것을 가시성이라고 한다.

Volatile 변수와 가시성

Cache에서 메인 메모리로 값이 쓰는 것을 다른 책이나 문서에서는 flush라고 표현한다. volatilevolatile로 선언된 변수의 수정사항을 CPU에서 바로 메모리로 바로 flush하여 가시성을 확보한다. 하지만volatile 변수는 연산의 원자성을 보장하지 않는다. volatile 변수는 연산의 원자성은 보장하지 못하고 가시성만 보장한다.



Atomic

이 외에도 동시성을 제어하기 위한 방법으로 synchronized 키워드와 atomic을 활용한 방식이 있다. Synchronized는 메소드 영역에 설정해서 메소드 자체를 임계 영역(Critical Section)으로 설정해 동시 진입을 못하게 하는 방식이다. 진입 자체를 막는 방식이기 때문에 대기 시간으로 인한 속도 이슈가 발생한다. 어떤 Thread는 Lock을 확보하느라 또 다른 Thread는 Lock을 확보하지 못해 Blocking 상태로 들어가느라 그리고 이 상태가 변경되는 동안 많은 시스템 자원이 쓰인다고 한다. (Context Switching 비용) 결국 이 문제는 성능문제로 이어진다.

atomic 변수는 멀티 스레드 환경에서 원자성을 보장하기 위해 나온 개념이다. synchronized와는 다르게 blocking이 아닌 non-blocking하면서 원자성을 보장하여 동기화 문제를 해결한다. Atomic은 Compare-And-Swap(CAS) 알고리즘 방식을 사용한다. CAS는 Compare-And-Swap의 줄임말로 말 그대로 비교하고 변경하는 방식이다. 동작원리는 다음과 같다.


  1. 인자로 기존 값(Compared Value)과 변경할 값(Exchanged Value)을 전달한다.
  2. 기존 값(Compared Value)이 현재 메모리가 가지고 있는 값(Destination)과 같다면 변경할 값(Exchanged Value)을 반영하며 true를 반환한다.
  3. 반대로 기존 값(Compared Value)이 현재 메모리가 가지고 있는 값(Destination)과 다르다면 값을 반영하지 않고 false를 반환한다.

여기서 기존 값과 현재 메모리가 가지고 있는 값이 다른 경우라는게 어떤 건지 의문이 생길 수 있다. 이 말의 의미는 스레드 A가 공유 변수에 대해 계산을 하고 메모리에 반영하기 직전에 다른 스레드 B가 공유 변수를 변경하여 메모리에 반영한 경우를 의미한다. 이때 당연히 스레드 A의 변경할 값을 메모리에 반영하면 안 된다. 따라서 false를 반환하는 경우에는 무한 루프를 구성하여 변경된 값(다른 스레드에 의해 변경된 메모리 값)을 읽고 같은 시도를 반복하거나, 다른 더 중요한 작업이 있으면 다른 작업을 해도 된다. 이 부분은 개발자가 결정한다.

정리하자면 Atomic은 blocking 방식을 사용하는 synchronized에 비해 훨씬 효율적인 방법이라고 할 수 있다. 무한 루프를 돌면서 값을 반영할 수 있는지 물어보는 경우에도 스레드의 상태를 변경하는 작업이 발생하지 않으므로 성능이 더 우수하다.

atomic 변수의 핵심 원리인 CAS 알고리즘은 원자성 뿐만 아니라 가시성 문제도 해결해 주는 것을 볼 수 있다. 그리고 non-blocking이 가능하므로 blocking 방식인 synchronized보다 성능 상 이점이 있다는 것도 알 수 있었다. 참고로 synchronized 키워드의 경우 synchronized 블록에 진입하기 전에 CPU 캐시 메모리와 메인 메모리 값을 동기화하여 가시성을 해결한다.



Reference

https://steady-coding.tistory.com/568