멀티스레드와 동시성

Ch10. 동기화와 원자적 연산(CAS) - CAS 정리

webmaster 2024. 9. 15. 11:59
728x90

CAS의 장점

  • 낙관적 동기화: 락을 걸지 않고도 값을 안전하게 업데이트할 수 있다. CAS는 충돌이 자주 발생하지 않을 것이라고 가정한다. 이는 충돌이 적은 환경에서 높은 성능을 발휘한다. 
  • 락 프리(Lock-Free): CAS는 락을 사용하지 않기 때문에, 락을 획득하기 위해 대기하는 시간이 없다. 따라서 스레드가 블로킹되지 않으며, 병렬 처리가 더 효율적일 수 있다. 

CAS의 단점

  • 충돌이 빈번한 경우: 여러 스레드가 동시에 동일한 변수에 접근하여 업데이트를 시도할 때 충돌이 발생할 수 있다. 충돌이 발생하면 CAS는 루프를 돌며 재시도해야 하며, 이에 따라 CPU 자원을 계속 소모할 수 있다. 반복적인 재시도로 인해 오버헤드가 발생할 수 있다. 
  • 스핀락과 유사한 오버헤드: CAS는 충돌 시 반복적인 재시도를 하므로, 이 과정이 계속 반복되면 스핀락과 유사한 성능 저하가 발생할 수 있다. 특히 충돌 빈도가 높을수록 이런 현상이 두드러진다. 

동기화 락의 장점

  • 충돌 관리: 락을 사용하면 하나의 스레드만 리소스에 접근할 수 있으므로 충돌이 발생하지 않는다. 여러 스레드가 경쟁할 경우에도 안정적으로 동작한다. 
  • 안정성: 복잡한 상황에서도 락은 일관성 있는 동작을 보장한다.
  • 스레드 대기: 락을 대기하는 스레드는 CPU를 거의 사용하지 않는다. 

동기화 락의 단점

  • 락 획득 대기 시간: 스레드가 락을 획득하기 위해 대기해야 하므로, 대기 시간이 길어질 수 있다. 
  • 컨텍스트 스위칭 오버헤드: 락을 사용하면, 락 획득을 대기하는 시점과 또 락을 획득하는 시점에 스레드의 상태가 변경된다. 이때 컨텍스트 스위칭이 발생할 수 있으며, 이로 인해 오버헤드가 증가할 수 있다. 

결론
일반적으로 동기화 락을 사용하고, 아주 특별한 경우에 한정해서 CAS 사용해서 최적화해야 한다. CAS를 통한 최적화가 더 나은 경우는스레드가 "RUNNABLE" -> "BLOCKED" , "WAITING" 상태에서 다시 "RUNNABLE" 상태로 가는 것 보다는, 스레드를 "RUNNABLE" 살려둔 상태에서 계속 획득을 반복 체크하는 것이 효율적인 경우에 사용해야 한다. 하지만 경우 대기하는 스레드가 CPU 자원을 계속 소모하기 때문에 대기 시간이 아주아주아주 짧아 한다. 따라서 임계 영역이 필요는 하지만, 연산이 길지 않고 매우매우매우! 짧게 끝날 사용해야 한다.
예를 들어 숫자 값의 증가, 자료 구조의 데이터 추가, 삭제와 같이 CPU 사이클이 금방 끝나지만 안전한 임계 영역, 또는 원자적인 연산이 필요한 경우에 사용해야 한다. 반면에 데이터베이스를 기다린다거나, 다른 서버의 요청을 기다리는 것 처럼 오래 기다리는 작업에 CAS 사용하면 CPU 계속 사용하며 기다리는 최악의 결과가 나올 수도 있다. 이런 경우에는 동기화 락을 사용해야 한다.

또한 CAS는 충돌 가능성이 낮은 환경에서 매우 효율적이지만, 충돌 가능성이 높은 환경에서는 성능 저하가 발생할 수 있다. 이런 경우에는 상황에 맞는 적절한 동기화 전략을 사용하는 것이 중요하다. 때로는 락이 더 나은 성능을 발휘할 수 있으며, CAS가 항상 더 빠르다고 단정할 수는 없다. 따라서, 각 접근 방식의 특성을 이해하고, 애플리케이션의 특정 요구사항과 환경에 맞는 방식을 선택하는 것이 필요하다.

 

실무 관점
실무 관점에서 보면 대부분의 애플리케이션들은 공유 자원을 사용할 때, 충돌할 가능성 보다 충돌하지 않을 가능성이 훨씬 높다. 예를 들어서 여러 스레드에서 발생하는 주문 수를 실시간으로 증가하면서 카운트한다고 가정해 보자. 그리고 특정 피크시간에 주문이 100만건 들어오는 서비스라고 가정해보자.

1,000,000 / 60분 = 1분에 16,666건, 1초에 277건

CPU가 1초에 얼마나 많은 연산을 처리하는지 생각해 보면, 백만 건 중에 충돌이 나는 경우는 아주 넉넉하게 해도 몇십 건 이하일 것이다. 따라서 실무에서는 주문 수 증가와 같은 단순한 연산의 경우, 락을 걸고 시작하는 것보다는, CAS처 럼 낙관적인 방식이 더 나은 성능을 보인다.

그런데 여기서 중요한 핵심은 주문 수 증가와 같은 단순한 연산이라는 점이다. 이런 경우에는 AtomicInteger와 같은 CAS 연산을 사용하는 방식이 효과적이다. 이런 연산은 나노 초 단위로 발생하는 연산이다. 반면에 데이터베이스를 기다린다거나, 다른 서버의 요청을 기다리는 것처럼 수 밀리초 이상의 시간이 걸리는 작업이라 면 CAS를 사용하는 것보다 동기화 락을 사용하거나 스레드가 대기하는 방식이 더 효과적이다.

우리가 사용하는 많은 자바 동시성 라이브러리들, 동기화 컬렉션들은 성능 최적화를 위해 CAS 연산을 적극 활용한다. 덕분에 실무에서 직접 CAS 연산을 사용하는 사용하는 일은 매우 드물다. 대신에 CAS 연산을 사용해서 최적화되어 있는 라이브러리들을 이해하고 편리하게 사용할 줄 알면 충분하다. CAS의 개념을 알아두면 앞으로 멀티스레드와 관련된 다양한 라이브러리들을 분석할 때, "아~ 이 부분은 CAS를 사용해 서 최적화 했구나"라는 점을 이해할 수 있을 것이다.

 

참고: CAS 연산은 심화 내용이다. 이해가 어렵다면 가볍게 듣고 넘어가도 괜찮다. 왜냐하면 우리가 직접 CAS 연산을 사용하는 경우는 거의 없기 때문이다. 대부분 복잡한 동시성 라이브러리들이 CAS 연산을 사용한다. 우리는 AtomicInteger 같은 CAS 연산을 사용하는 라이브러리들을 사용하는 정도면 충분하다.

728x90