JAVA

[Java] 동시성 문제

로춘남 2022. 3. 28. 11:40
728x90


컴퓨터 CPU와 RAM의 관계도

CPU가 어떤 작업을 처리하기 위해 데이터가 필요할 때, CPU는 RAM의 일부분을 고속의 저장 장치인 CPU Cache Memory로 읽어들인다. 이 읽어들인 데이터로 명령을 수행하고 이 데이터를 다시 RAM에 저장하기 위해서는 데이터를 읽어들일 때의 과정을 역순으로 밟는다.

즉, 적절한 시점에 CPU Cache Memory에서 RAM으로 쓰기 작업을 하게 된다. 그러나 CPU가 캐시에 쓰기 작업을 수행했다고 해서 바로 RAM으로 쓰기 작업을 수행하지 않는다. 반대의 과정인 읽기 작업도 마찬가지이다.

동시성 프로그래밍에서는 CPU와 RAM의 중간에 위치하는 CPU Cache Memory와 병렬성이라는 특징 때문에 다수의 스레드가 공유 자원에 접근할 때 2가지의 문제가 발생 될 수 있다.

가시성 문제

가시성 문제는 여러 개의 스레드가 사용됨에 따라, CPU Cache Memory와 RAM의 데이터가 서로 일치하지 않아 생기는 문제를 의미한다. 이를 해결하기 위해서는 가시성이 보장되어야 하는 변수들을 CPU Cache Memory에서 불러오는 것이 아닌 RAM에서 바로 읽도록 보장해야 한다. 변수에 volatile 키워드를 붙여줘서 가시성을 보장할 수 있다.

private static volatile boolean isStop;

그러나 가시성만 보장된다고 동시성이 보장되는 것이 아니다. 

전철 비용을 계산하는 프로그램을 만든다고 가정을 하면, 이 때 나이에 따라서 70세 미만과 70세 이상의 표 값이 다른 상황이고, 날짜를 실시간으로 반영하여 비용을 계산해야 한다.

각 스레드의 역할은 다음과 같다.

  • Thread 1
    • 고객의 나이를 읽는다.
    • 읽어온 나이를 기준으로 비용을 계산한다.
    • 비용을 반환한다.
  • Thread 2
    • 현재 년도를 지속적으로 읽는다.
    • 해가 바뀌면 고객의 나이를 계산한다.
    • 바뀐 고객의 나이를 저장한다.

volatile을 통해 가시성을 해결했지만, 해가 바뀌는 시점에 문제가 발생한다.

  1. 나이가 69살인 고객이 계산을 진행할 때, 첫 번째 스레드에서는 RAM에서 나이가 69세임을 가져와 나이에 따라 비용을 계산하는 메소드를 실행한다.
  2. 그러나 마침 해가 딱 바뀌어 년 바뀜을 계산하는 두 번째 스레드에서 RAM에 해당 고객의 나이를 70살로 수정한다.
  3. 첫 번째 스레드는 나이가 70살로 바뀐 것을 모르고 69살로 계산하여 전철 비용을 계산해서 잘못된 비용을 반환한다.

이렇게 가시성이 보장된다고 동시성이 보장되는 것은 아니다. 그러나 volatile 만으로 동시성이 보장되는 경우도 있는데, 이러한 경우는 하나의 스레드만이 연산을 해야 한다라는 전제가 깔려있다. 이 전제가 확실한 경우 lock없이 volatile만으로도 문제 없는 데이터를 사용할 수 있다.

원자성 문제

원자성은 가시성과 멀티 스레드 환경에서 스레드간 공유 메모리 이슈를 발생시킨다는 점에 공통점을 가지고 있다. 하지만 시스템 관점에서 보면 두 개념은 다른 곳에 존재한다.

  • 가시성
    • CPU - Cache - Memory 관계 상의 개념
  • 원자성
    • 한줄의 프로그램 문장이 컴파일러에 의해 기계어로 변경되면서, 이를 기계가 순차적으로 처리하기 위한 여러 개의 Machine Instruction이 만들어져 실행되기 때문에 일어나는 현상을 설명하는 용어

공유되는 변수를 변경할 때 기존의 값을 기반으로 새로운 값이 결정되는 과정에서 여러 스레드가 이를 동시에 수행할 때 생기는 이슈를 원자성이라 부른다.

예를 들어, i++ 연산을 2개의 스레드가 동시에 100회 실행한다고 가정해보자. 만약 i++ 연산이 원자성을 가지고 있는 연산이라고 하면 결과가 200이여야 하겠지만, 실제로 프로그램을 수행하면 200보다 작은 값이 출력된다. 이런 결과의 원인은 i++가 3개의 instruction으로 이루어져 있기 때문에 스레드 A가 i값을 읽어 i+1 연산을 해서 메모리에 반영하기 직전에 스레드 B가 i+1 연산을 수행하고 메모리에 반영을 한다면 후자의 연산은 무효가 되기 때문이다.

이러한 원자성 문제를 해결하기 위해서는 synchronized 또는 atomic을 사용해야 한다.

참고로 원자성 문제를 synchronized 또는 atomic을 통해 해결한다면 가시성의 문제도 해결된다. 

synchronized 블럭을 들어가기 전에 CPU Cache Memory와 Main Memory를 동기화 해주며, atomic의 경우에는 CAS 알고리즘에 의해 원자성 문제와 CPU Cache Memory에 잘못된 값을 참조하는 문제를 동시에 해결해주기 때문이다.

synchronized (blocking)

synchronized는 멀티 스레드 환경에서 동시성 제어를 위해 공유 객체를 동기화하는 키워드이다. synchronized 블록 안에서 관리되는 자원들은 원자성을 보장할 수 있다.

atomic (non-blocking)

atomic 또한 멀티 스레드 환경에서 원자성을 보장하기 위해 나온 개념이다. 동기화(blocking)가 아닌 CAS(Compared And Swap)라는 알고리즘으로 작동하여 원자성을 보장한다.

 

☞ 동기와 비동기, blocking과 non-blocking

 

※ CAS 알고리즘이란?

CPU Cache Memory와 RAM을 비교하여 일치한다면 CPU Cache Memory와 RAM에 적용하고, 일치하지 않는다면 재시도하여 어떠한 쓰레드에서 공유 자원에 읽기/쓰기 작업을 하더라도 원자성을 보장한다. 대표적인 예시로는 Java의 Concurrent 패키지의 타입들은 CAS 알고리즘을 이용하여 원자성을 보장한다.


Q. 동시성 프로그래밍에서 발생 할 수 있는 문제는?

: 동시성 프로그램에서는 CPU와 RAM의 중간에 위치하는 CPU Cache Memory와 병렬성이라는 특징때문에 가시성, 원자성 문제가 발생 할 수 있다. CPU가 어떠한 작업을 위해 RAM에 저장되어 있는 일부분을 CPU Cache Memory로 읽어들이는데, 작업을 수행하고 나면 CPU Cache Memory에서 Ram으로 데이터를 쓰게된다. 하지만 이러한 과정에서 CPU 작업이 끝난 직후 RAM에 데이터를 쓰는것이 아니라(가시성 문제) 위 이유와 함께 RAM의 데이터를 CPU Core1과 Core2에서 동시에 읽고, RAM에 쓰는 시점은 다를 때(동시성 접근 문제) 발생하게 되는 문제를 동시성(병렬성) 문제라고 본다.

 

가시성 문제

  • 여러개의 스레드가 사용됨에 따라 CPU Cache Memory와 RAM의 데이터가 서로 일치하지 않아 생기는 문제를 의미. 가시성을 보장되어야하는 변수에 volatile 키워드를 붙여줘서 RAM에서 바로 읽도록 해야한다. 그러나 여러 스레드가 공유 자원에 쓰기 연산을 할 경우 가시성을 보장했다고해서 동시성이 보장되지 않는다.

원자성 문제

여러 스레드가 공유 자원에 동시에 쓰기 연산을 할 경우 잘못된 결과를 반환하는것을 의미. 따라서 synchronized, atomic을 통해 원자성을 보장해야한다.


※ 동시성 vs 병렬성

동시성(Concurrency)

  • 한 CPU에서 동시에 여러 작업을 하는것처럼 보이게 만드는 것
  • 동시에 실행되는것 같이 보이는 것
  • 싱글 코어에서 멀티 쓰레드를 동작 시키는 방식
  • 한번에 많은것을 처리
  • 한 cpu에서 2개의 process가 있을경우, 이 두 프로세스는 매우 짧은 시간동안 context switching이 일어나면서 번갈아서 실행된다

병렬성(Parallelism)

  • 다중 CPU에서 작업이 병렬적으로 실행. 하나의 CPU당 코드가 실행
  • 실제로 동시에 여러 작업이 처리 되는 것
  • 멀티 코어에서 멀티 쓰레드를 동작시키는 방식
  • 한번에 많은 일을 처리
  • 데이터 병렬성과 작업 병렬성으로 구분
    • 데이터 병렬성(Data Parallelism) : 전체 데이터를 멀티 코어의 수만큼 서브 데이터들로 나눈뒤, 병렬처리하여 작업을 수행하는 것. Java 8 에서 지원하는 병렬 스트림이 데이터 병렬성을 구현한 것.
    • 작업 병렬성(Task Parallelism) : 서로 다른 작업을 병렬 처리하는것. 웹 서버는 각각의 브라우저에서 요청한 내용을 개별 쓰레드에서 병렬 처리함.

  • 일반적인 성능 : 동시성 > 병렬성 (CPU와 하드웨어를 쓰는 자원이 동시성이 병렬성에 비해 적기 때문)
  • 동시성은 병렬성이기 위한 필요조건이지만 충분조건은 아니다
    • 병렬성을 만족하면, 동시성도 만족하게 된다.
    • 동시성을 만족한다고 병렬성을 만족하는 것은 아니다.
  • 병렬성이 가지는 동시성과의 차이점 : 각 코어내의 스레드가 실제로 동시에 명령어를 실행할 수 있다.

출 처 

728x90