You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
그런데 생각해보면 이 명령어는 2개로 나누어진 명령어이다. 따라서 원자적이지 않은 연산처럼 보인다.
먼저 메인 메모리에 있는 값을 확인한다.
해당 값이 기대하는 값(0)이라면 원하는 값(1)으로 변경한다.
CPU 하드웨어의 지원
CAS 연산은 이렇게 원자적이지 않은 두 개의 연산을 CPU 하드웨어 차원에서 특별하게 하나의 원자적인 연산으로 묶 어서 제공하는 기능이다. 이것은 소프트웨어가 제공하는 기능이 아니라 하드웨어가 제공하는 기능이다. 대부분의 현대 CPU들은 CAS 연산을 위한 명령어를 제공한다.
CPU는 다음 두 과정을 묶어서 하나의 원자적인 명력으로 만들어버린다. 따라서 중간에 다른 스레드가 개입할 수 없다.
x001의 값을 확인한다.
읽은 값이 0이면 1로 변경한다.
CPU는 두 과정을 하나의 원자적인 명령으로 만들기 위해 1번과 2번 사이에 다른 스레드가 x001의 값을 변경하지 못하게 막는다. 참고로 1번과 2번 사이의 시간은 CPU 입장에서 보면 아주 잠깐 찰나의 순간이다. 그래서 성능에 큰 영향을 끼치지 않는다.
value 의 값이 0 → 1이 되었다.
CAS 연산으로 값을 성공적으로 변경하고 나면 true 를 반환한다.
CAS - 실패 케이스
CAS연산은 메모리에 있는 값이 기대하는 값이라면 원하는 값으로 변경한다.
여기서는 AtomicInteger 내부에 있는 value 값이 0이라면 1로 변경하고 싶다.
현재 value 의 값이 기대하는 0이 아니라 1이므로 아무것도 변경하지 않는다.
CAS 연산으로 값 변경에 실패하면 false 를 반환하고, 값도 변경하지 않는다.
📌 CAS 연산2
어떤 값을 하나 증가하는 value++ 연산은 원자적 연산이 아니다.
i = i + 1;
이 연산은 다음 순서로 나누어 실행된다. i 의 초기 값은 0으로 가정하겠다.
오른쪽에있는 i 의값을읽는다. i 의값은0이다.
읽은 0에 1을 더해서 1을 만든다.
더한 1을 왼쪽의 i 변수에 대입한다.
1번과 3번 연산 사이에 다른 스레드가 i의 값을 변경할 수 있기 때문에 문제가 될 수 있다. 따라서 value++ 연산을 여러 스레드에서 사용한다면, 락을 건 다음에 값을 증가해야 한다.
atomicInteger.get() 을 사용해서 value 값을 읽는다. ⇒ getValue 는 0이다.
compareAndSet(0,1) 을 수행한다.
compareAndSet(getValue, getValue + 1)
그런데 compareAndSet(0,1) 연산은 실패한다.
CAS 연산에서 현재 value 값으로 0을 기대했지만 Thread-1 이 중간에 먼저 실행되면서 value 의 값을 0 → 1로 변경해버렸다.
CAS 연산이 실패했으므로 value 값은 변경하지 않고, false 를 반환한다.
실패했으므로 dowhile 문을 빠져나가지 못한다. dowhile 문을 다시 시작한다.
while (!result) → while(!false) → while(true) 이므로 다시 반복
[Thread-0] do~while 두 번째 시도
do~while 문이 다시 시작된다.
atomicInteger.get() 을 사용해서 value 값을 읽는다. getValue 는 1이다.
compareAndSet(1,2) 을 수행한다.
compareAndSet(getValue, getValue + 1)
CAS 연산이 성공했으므로 value 값은 1에서 2로 증가하고 true 를 반환한다.
do~while 문을 빠져나간다.
정리
AtomicInteger 가 제공하는 incrementAndGet() 코드도 앞서 우리가 직접 작성한 incrementAndGet() 코드와 똑같이 CAS를 활용하도록 작성되어 있다. CAS를 사용하면 락을 사용하지 않지만, 대신에 다른 스레드가 값을
먼저 증가해서 문제가 발생하는 경우 루프를 돌며 재시도를 하는 방식을 사용한다.
이 방식은 다음과 같이 동작한다.
현재 변수의 값을 읽어온다.
변수의값을1증가시킬때,원래값이같은지확인한다.(CAS연산사용)
동일하다면 증가된 값을 변수에 저장하고 종료한다.
동일하지 않다면 다른 스레드가 값을 중간에 변경한 것이므로, 다시 처음으로 돌아가 위 과정을 반복한다.
두 스레드가 동시에 실행되면서 문제가 발생하는 상황을 스레드가 충돌했다고 표현한다.
이 과정에서 충돌이 발생할 때마다 반복해서 다시 시도하므로, 결과적으로 락 없이 데이터를 안전하게 변경할 수 있다.
CAS를 사용하는 방식은 충돌이 드물게 발생하는 환경에서는 락을 사용하지 않으므로 높은 성능을 발휘할 수 있다. 이 는 락을 사용하는 방식과 비교했을 때, 스레드가 락을 획득하기 위해 대기하지 않기 때문에 대기 시간과 오버헤드가 줄 어드는 장점이 있다.
그러나 충돌이 빈번하게 발생하는 환경에서는 성능에 문제가 될 수 있다. 여러 스레드가 자주 동시에 동일한 변수의 값 을 변경하려고 시도할 때, CAS는 자주 실패하고 재시도해야 하므로 성능 저하가 발생할 수 있다. 이런 상황에서는 반복 문을 계속 돌기 때문에 CPU 자원을 많이 소모하게 된다.
CAS(Compare-And-Swap)와 락(Lock) 방식의 비교
락(Lock) 방식
비관적(pessimistic) 접근법
데이터에 접근하기 전에 항상 락을 획득
다른 스레드의 접근을 막음
"다른 스레드가 방해할 것이다"라고 가정
CAS(Compare-And-Swap) 방식
낙관적(optimistic) 접근법
락을 사용하지 않고 데이터에 바로 접근
충돌이 발생하면 그때 재시도
"대부분의 경우 충돌이 없을 것이다"라고 가정
앞서 여러 스레드가 value++ 연산을 수행했던 BasicInteger, VolatileInteger 의 예
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
-
📌 CAS 연산1
락 기반 방식의 문제점
예를 들어서 락을 사용하는 연산이 있다고 가정하자. 락을 사용하는 방식은 다음과 같이 작동한다.
락이 있는지 확인한다.
락을 획득하고 임계 영역에 들어간다.
작업을 수행한다.
락을 반납한다.
⇒ 여기서는 락을 획득하고 반납하는 과정이 계속 반복된다. (락을 사용하는 방식은 직관적이지만 상대적으로 무거운 방식이다.)
CAS
실행결과
compareAndSet(0, 1)
한다.
실행 순서 분석
CPU 하드웨어의 지원
CAS - 실패 케이스
false를 반환하고, 값도 변경하지 않는다.📌 CAS 연산2
이 연산은 다음 순서로 나누어 실행된다. i 의 초기 값은 0으로 가정하겠다.
1번과 3번 연산 사이에 다른 스레드가 i의 값을 변경할 수 있기 때문에 문제가 될 수 있다. 따라서 value++ 연산을 여러 스레드에서 사용한다면, 락을 건 다음에 값을 증가해야 한다.
CAS 연산을 활용해서 락 없이 값을 증가하는 기능 구현
실행 결과
지금은 순서대로 실행되기 때문에 결과는 다음과 같다.
incrementAndGet 첫 번째 실행
incrementAndGet 두 번째 실행
지
지금은 main 스레드 하나로 순서대로 실행되기 때문에 CAS 연산이 실패하는 상황을 볼 수 없다. 우리가 기대하는 실패하는 상황은 연산의 중간에 다른 스레드가 값을 변경해버리는 것이다. 멀티스레드로 실행해서 CAS 연산이 실패하는 경우 어떻게 작동하는지 알아보자.
📌 CAS 연산3
실행결과
두 스레드의 실행결과 분석
Thread-1 실행
Thread-0 실행
[Thread-0] do~while 첫 번째 시도
while 문을 빠져나가지 못한다. dowhile 문을 다시 시작한다.while (!result) → while(!false) → while(true) 이므로 다시 반복
[Thread-0] do~while 두 번째 시도
정리
AtomicInteger 가 제공하는 incrementAndGet() 코드도 앞서 우리가 직접 작성한 incrementAndGet() 코드와 똑같이 CAS를 활용하도록 작성되어 있다. CAS를 사용하면 락을 사용하지 않지만, 대신에 다른 스레드가 값을
먼저 증가해서 문제가 발생하는 경우 루프를 돌며 재시도를 하는 방식을 사용한다.
이 방식은 다음과 같이 동작한다.
두 스레드가 동시에 실행되면서 문제가 발생하는 상황을 스레드가 충돌했다고 표현한다.
이 과정에서 충돌이 발생할 때마다 반복해서 다시 시도하므로, 결과적으로 락 없이 데이터를 안전하게 변경할 수 있다.
CAS를 사용하는 방식은 충돌이 드물게 발생하는 환경에서는 락을 사용하지 않으므로 높은 성능을 발휘할 수 있다. 이 는 락을 사용하는 방식과 비교했을 때, 스레드가 락을 획득하기 위해 대기하지 않기 때문에 대기 시간과 오버헤드가 줄 어드는 장점이 있다.
그러나 충돌이 빈번하게 발생하는 환경에서는 성능에 문제가 될 수 있다. 여러 스레드가 자주 동시에 동일한 변수의 값 을 변경하려고 시도할 때, CAS는 자주 실패하고 재시도해야 하므로 성능 저하가 발생할 수 있다. 이런 상황에서는 반복 문을 계속 돌기 때문에 CPU 자원을 많이 소모하게 된다.
CAS(Compare-And-Swap)와 락(Lock) 방식의 비교
앞서 여러 스레드가 value++ 연산을 수행했던 BasicInteger, VolatileInteger 의 예
BasicInteger 의 실행 결과를 보면 최대한 스레드를 충돌하게 만들었는데도, 1000개 중에 약 50개의 스레드만 충돌한 사실을 확인할 수 있다.
락 방식
CAS 방식
⇒ 정리하면 간단한 CPU 연산에는 락 보다는 CAS를 사용하는 것이 효과적이다.
Beta Was this translation helpful? Give feedback.
All reactions