JAVA

동시성

whyWhale 2021. 5. 31.

직렬성, 동시성, 병렬성


직렬성

  • 각각의 Transaction이 일정한 순서를 가지고 순차적으로 실행되는 것을 의미.

동시성

  • Transcacion들이 순차적이 아닌 transaction을 구성하는 각각의 쿼리문들이 transacion의 순서에 상관없이 동시에 실행되는 것을 의미.
    • 한 CPU에서 동시에 여러 작업을 하는 것 처럼 보이게 만든다.
    • 싱글 코어에서 멀티 쓰레드를 동작하는 방법.
      • 여러 개의 스레드가 번갈아가면서 실행되는 성질을 말한다.
      • 멀티 스레드로 동시성을 만족시킬 수 있는 것이다. 동시성과 멀티스레
    • 한 꺼번에 많은 것을 처리하는 논리적인 개념이다.
    • 한 CPU에 2개 이상의 Process들이 있을 때 , CPU는 빠르게 Context Swithching이 일어나면서 작업을 처리하기 때문에 동시에 여러 작업을 하는 것 처럼 보이게 된다.
      • 싱글 코어에서 멀티 스레드를 이용해 동시성을 구현하는 일부 케이스에 대한 내용이다. 멀티 코어에서 멀티 스레드를 이용하여 동시성을 만족할 경우에는 실제 물리적 시간으로 동시에 실행된다.

병렬성

  • 2개 이상의 task 가 있을 때 각 task 가 물리적인 시간으로 동시에 실행이 가능하다는 의미.
    • 실제로 작업이 동시에 처리되는 것이다.(Concurrentcy 와 simultaneously를 둘다 만족하는 것을 의미)
    • 멀티 코어에서 멀티 쓰레드를 동작하는 방법.
      • 멀티 코어에서 멀티 스레드를 동작시키는 방식으로, 한 개 이상의 스레드를 포함하는 각 코어들이 동시에 실행되는 성질을 말한다.
      • 위 의 말은 부분적으로만 맞는 내용이다. 병렬성의 핵심은 물리적인 시간에 동시에 수행되는 것이지 멀티 코어에 포커스가 맞춰져서는 안된다. 그 예로 네트워크 상의 여러 컴퓨터에게 분산작업을 요청하는 분산 컴퓨팅이 있다.
    • 한 번에 많은 일을 처리하는 물리적인 개념이다.
    • 실제로 동시에 여러 작업이 처리되는 것이다.

 

 

직렬성, 동시성, 병렬성

+추가 설명

 

  • 위 그림은 쓰레드가 아닌 각 task가 수행하는 타임라인을 나타낸 것이다..
  • 위 그림은 Concurrently 진행된다고 애기할 수 있지만, Simultaneously 진행된다고 확실할 수 있다.

 

  • 좀 low level 측면에서 보자면,

위 케이스는 low level 에서 Task A, B 가 수행될 수 있는 일부 경우의 수를 나열한 것이다. 각 케이스에 대한 상태는 다음과 같다.

  • Case 1 은 concurrently 하게 진행되며, simultaneously 하게 진행되지는 않는다.
  • Case 2 는 concurrently 하고 simultaneously 하게 진행된다.

CS 내에서 각 단어에 대한 의미

  • concurrently : 2개 이상의 ask (= 코드, 알고리즘 등) 를 수행할 때, 각 task 는 다른 task 의 수행시점에 상관없이 수행이 가능하다는 의미이다. (sequencial 의 반의어이다.) 즉, N 개의 task 의 실행 시간이 타임라인 상에서 겹칠 수 있다.
  • simultaneously : 우리가 일반적으로 사용하는 ’동시에’ 라는 단어의 맥락과 일치한다.

각 케이스를 다시 살펴보자면,

  • Case 1 은 먼저 시작된 task 가 끝나지 않아도 또 다른 task 를 시작할 수 있다. 즉, 동시성을 만족한다. 하지만 적절하게 시간을 나눠 자원을 interiving 하고 있으므로 물리적으로 정확히 같은 시간대에 동시에 수행되는 것은 아니다. 따라서 병렬성은 만족하지 않는다.
  • Case 2 는 먼저 시작된 task 가 끝나지 않아도 또 다른 task 를 시작할 수 있다. 즉, 동시성을 만족한다. 또한 같은 시간대에 각 task 가 동시에 수행되므로 병렬성을 만족한다.

위 케이스들을 통해 알 수 있는 것은 동시성은 병렬성이기 위한 필요조건이지만 충분조건은 아니다. 즉,

  • 병렬성을 만족하면, 동시성도 만족하게 된다.
  • 하지만 동시성을 만족한다고 병렬성을 만족하는 것은 아니다.
더보기

예제 케이스

마지막으로 개념을 완전히 이해하는데 도울 수 있도록 몇가지 예제 케이스를 정의한다.

각 task 가 수행되는 시간을 1이라고 가정한다.

  • 싱글 코어, 1개의 쓰레드에서 순차적으로 2개의 task 진행 - 동시성과 병렬성 모두 만족하지 못함. seqeuncial 한 작업. 총 수행시간은 2
  • 싱글 코어, 1개의 쓰레드에서 2개의 task 가 자원을 interleaving 하며 진행 - 동시성을 만족하지만 병렬성을 만족하지 못함. 총 수행 시간은 2 + @ (있을수도 없을 수도 있다. interleaving 코스트)
  • 싱글 코어, 2개의 쓰레드에서 2개의 task 가 자원을 interleaving 하며 진행 - 동시성을 만족하지만 병렬성은 만족하지 못함. 총 수행 시간은 2보다 작다.
  • 멀티 코어, 2개의 쓰레드에서 2개의 task 가 동시에 진행 - 동시성, 병렬성을 모두 만족함. 총 수행 시간은 2보다 작다.
  • (보너스) 코틀린 내에서 싱글 코어, 1개의 쓰레드내에서 2개의 코루틴을 통해 2개의 task 가 진행 - 동시성은 만족하지만 병렬성은 만족하지 못함. 총 수행 시간은 2보다 작으며, 2개의 쓰레드를 사용했을 때보다 코스트가 낮다.

 

ref. https://vagabond95.me/posts/concurrency_vs_parallelism/

 


 

 

가시성 문제


하나의 스레드에서 공유 자원(변수, 객체)을 수정한 결과가 다른 스레드에게 보이지 않을 수 있다.

  • CPU Cache Memory의 병렬성
    • 메인 스레드와 백그라운드 스레드 실행시 서로 다른 결과값을 가지고 있다.
    • mainThread에서
      • CPU Cache Memory 2와 RAM에 공유 변수를 쓰기 작업을 완료했지만
    • backgroundThread에서
      • CPU Cache Memory 1에서 읽은 여전히 업데이트 되지 않은 변수 값을 사용한다.

즉 한개의 캐시메모리에서 메인 메모리에 쓰기 작업이 반영되기 전에 이미 다른 캐시메모리가 수정되지 않는 값을 읽어감으로써 서로 다른 값을 보게 되는 현상이다.

 

문제 해결

  • 변수 타입 앞에 volatile 을 선언한다. 
  • 그러면 Cpu cache memory에 거치지 않고 Ram으로 직접 읽고 쓰는 작업을 바로 수행함으로써 변수 값 불일치 문제를 해결할 수 있다.

하지만 멀티스레드 환경에서 여러 Thread가 write 하는 곳에는 적절하지 않다. 설마 쓰기 작업 도중 누군가가 읽어 간다면 똑같은 문제가 발생한다. 그래서 syncronized를 통해 원자성을 보장한다. 즉 하나의 쓰레드에서하는 R&W하고 나머지는 Read하는 상황에서 적절하다.

 

비용적인 측면에서 Cpu cache보다 메인 메모리의 비용이 더 크기 때문에 변수 값 일치성을 보장하는 경우에만 volatile 키워드를 사용하는 것이 좋다.

 

 

자원 동시 접근 문제


여러 스레드에서 공유 자원에 동시에 접근하여 변경했을 때 문제가 발생.

 

  • CPU Cache Memory의 병렬성
    • 메인 스레드와 백그라운드 스레드 실행시 서로 다른 같은 값을 가지고 간다.
    • mainThread에서
      • CPU Cache Memory 2와 RAM에 공유 변수를 쓰기 작업을 한다.
    • backgroundThread에서
      • CPU Cache Memory 1에서도 공유 변수를 쓰기 작업을 한다.

공유 변수 int cnt=0; 각 스레드는 cnt++ * 100 번을 하게 되면 총 cnt=200이 출력되어야 한다.

그런데 200보다 적은 값을 출력한다.

 

 

각 스레드의 캐시 메모리가 3이란 cnt값을 가져와 ++를 한다. 양쪽에서 4란 값을 메인 메모리에 연속으로 값이 중복된다. 이것이 동시 접근 문제이다.

 

 

문제 해결

  • synchronized 키워드(동시 접근 문제 해결)
  • lock을 이용하여 스레드가 공유 자원에 접근시 하나의 스레드만 공유 자원에 접근 할 수 있도록 한다.
  • 타입 반환 앞에 syncronized 입력.
  • 가시성 동시접근 문제를 해결할 수 있다.

 

synchronized 키워드

  • A 쓰레드의 작업이 끝날 때까지 대기해 라는 말이다.
  • synchronized 블럭 내에 있는 공유 자원을 점유하라!라고 이해를 하는 게 좀 더 정확하다.

쓰레드 로컬 변수

CPU에서 명령을 수행하기 위해서는 메모리에 있는 데이터를 CPU로 가져와야한다.
하지만 메모리에 있는 데이터를 CPU로 가져오는 행위는 매우 느리므로 CPU는 캐시 메모리가 있다. (L1 캐시, L2 캐시 등등) 그리고 이 캐시 메모리를 쓰레드 로컬 변수라고 부른다. 하지만 쓰레드 로컬 변수이기 때문에 다른 쓰레드에서는 메모리에 접근을 해도 해당 쓰레드 변수의 값을 얻어올 수 없다. 따라서 공유 자원 A’에 대해 각각 쓰레드가 로컬 변수를 가질 수 있게 된다. 여기서 synchronized 키워드가 가지는 진정한 의미가 나온다. synchronized 키워드를 사용한다는 것은 해당 블럭 내에 있는 공유 자원 A’가 쓰레드 로컬 변수에서 램으로 써지기 까지 다른 쓰레드는 대기(block)하라라는 의미를 가진다. 쓰레드 로컬 변수가 램에 써진 순간 다른 쓰레드가 램에서 해당 값을 가져와서 작업할 수 있게 된다.
DB에서 커밋이 되기 전까지 해당 레코드를 조회하는 다른 커넥션은 lock에 빠지는 것과 비슷한 의미라고 받아들이면 된다. synchronized 키워드를 남발하게 되면 쓰레드 로컬 변수가 램에 써지기 전까지 다른 쓰레드는 block이 되므로 조심해서 사용해야한다. 또한 잘못 쓴다면 무한한 block을 유발하는 dead lock 이슈도 조심해야할 것이다.

 

'JAVA' 카테고리의 다른 글

orElseGet 과 orElse 차이  (0) 2022.08.25
객체 판별  (0) 2021.10.18
Blocking , NonBolocking  (0) 2021.05.23
Enum class  (0) 2021.05.17
인터페이스, 추상클래스  (0) 2021.05.16

댓글