오늘도 난 출근 준비를 한다.
아침에는 어찌나 일어나기가 싫은지 최대한 잠을 자게 된다. 그래서 난 늘 지각을 걱정하며 바삐 아침을 보낸다.
일어나서 식빵을 토스터기에 넣고 빵이 구워지는 동안 샤워를 한다. 샤워를 하며 뉴스를 듣고 오늘 무슨 옷을 입을 지 미리 생각한다. 이렇게하면 30분이면 출근 준비 완료다.
만약 이런 아침 준비 과정을 하나씩 진행한다면 족히 1시간은 걸리지 않을까..?
작업의 효율성
동시에 여러 일을 할 수 있다면 시간을 절약할 수 있다. 시간을 절약한다는 것은 곧 일의 효율이 올라간다는 것이다.
이 논리로 컴퓨터에서도 동시에 여러 작업을 처리하는 것이 빠를 것이라는 생각이 든다.
병렬 처리 vs 동시 처리
여러 작업을 처리 할때 고려해야 할 사항이 있다.
CPU는 멀티 코어를 지원하기 때문에 프로세스와 스레드를 동시에 작업을 수행한다. 쉽게 말해서 두가지 일을 각각 할 수 있는 것이다. 이것을 병렬 처리라 한다.
프로세스는 멀티 스레드를 이용해서 하나의 작업을 번갈아 가면서 수행하여 각각 작업을 수행 하는 것 처럼 보이게 한다. 이것을 동시처리라 한다.
*두가지 개념은 서로 다른 목표와 개념을 갖고 있기 때문에 구별지어 생각할 필요가 있다.
멀티 스레드를 사용해야 하는 이유
멀티 프로세스는 여러개의 프로세스가 작업을 하는 것이다.
하나의 프로세스가 수행중에 키보드 입력을 기다린다고 할때, cpu는 놀게 된다. 그렇다면 다음 작업을 미리 실행할 수가 없다.
그래서 멀티스레드를 사용하여 cpu가 쉬지 않고 작업을 더 효율적으로 수행 할 수 있다. 비결은 컨택스트 스위칭 비용 절감.
드디어 스레드 세이프티
멀티 스레드를 사용하려면 스레드 세이프티 해야한다.
스레드 세이프티 하지 않다면, 공유 데이터가 뒤죽박죽 엉망이 될 수 있다.
그렇다면 왜 멀티스레드에서 스레드 세이프티하지 않다면 데이터가 엉망이 될까?
스레드는 프로세스의 data, heap 영역을 공유한다. 이때 작업을 동시 처리하기 위해 컨택스트 스위칭이 발생한다.
이때 공유 변수를 동기화 해주지 않는다면 데이터가 꼬이게 된다.
CPU의 연산을 위해서는 RAM에 데이터를 올려줘야 한다. CPU 와 RAM 사이에 작업을 효율적으로 처리하기 위해 CPU Cache Memory가 있다.
이상적인 시나리오는 CPU에서 쓰기 작업 수행 시, RAM으로 바로 쓰기 작업을 수행하는 것이며,
CPU에서 읽기 작업 수행 시, RAM에서 공유 데이터를 읽어들여 바로 읽기 작업을 수행 할 수 있다면 좋겠다.
가시성
CPU에서 읽기 작업 수행 시, RAM에서 공유 데이터를 읽어들여 바로 읽기 작업을 수행 할 수 있다면 좋겠다.
하지만 여러 스레드가 사용되는 멀티 스레드 환경에서는 읽기 작업 수행 시, CPU Cache Memory 와 RAM의 데이터가 불일치 되는 문제가 생긴다.
왜냐하면 RAM에서 공유 데이터를 읽어 들였다고 해도 그 값을 언제 CPU Cache Memory에 업데이트 할지 알 수 없기 때문이다.
이런 문제를 가시성 문제라고 한다.
자바에서는 가시성 문제를 해결하기 위해 공유 변수에 volatile 키워드를 사용하여 해결한다.
하지만 가시성을 보장한다고 해서 동시성을 보장한다고는 할 수 없다.
아래 예시를 보자.
2024년 12월 31일 오후 11시 59분.
이때 현재 69살 노인이 기차표를 구매한다고 할때, 1분이 지났다. 첫번째 스레드에서는 나이가 업데이트 되기 전에 읽어 69살의 가격표 결과가 나왔다. 하지만 다른 스레드에서는 70살에 해당하는 가격표가 나왔다. 값에 동시성이 보장이 되지 않는 것을 확인 할 수 있다.
이럴때를 위해 volatile 키워드 사용하면 해결할 수있다.
한 스레드만 쓰기작업을 하고 나머지 스레드가 읽기만 하는경우에 해결이 된다.
따라서 가시성 보장은 동시성을 보장하지는 않는다.
원자성
“check-then-act”가 대표적으로 멀티스레드에서 원자성 이슈가 발생하는 사례이다.
- 변수를 확인하고 (읽기)
- (쓰기 혹은 수정) 동작을 수행한다.
count++ 을 한다고 가정 했을때, 카운트하기 위해선 한 스레드 내에서 1. count의 기존값을 확인해야하고(읽기) 2. 카운트 업 해야한다.(쓰기 혹은 수정)
이것을 2개의 스레드가 동시에 100회 수행된다고 하면 총 200이 나와야한다.
하지만 결과는 그보다 작은 값이 나오게 된다.
이유는 첫번째 동작한 스레드가 읽기 동작 하고 나서 다른 스레드가 마침 저장하는 타이밍이라면? 기껏 카운트 업 한 값이 무의미 해 질 것이다.
이렇게 1번과 2번사이에 다른 스레드로 전환되어 count 변수를 바꾸게 된다면 변수의 값은 지켜지지 않게 된다. 즉 race condition이 발생하게 된다.
위 문제를 해결하기 위해서는 해당 범위만큼 lock을 걸어 동기화를 하면 된다.
단순하다. 자바에서는 synchronize 키워드를 이용하면 된다.
락의 범위는 클래스단위 메서드단위 블록단위로 걸 수 있다.
하지만 블록을 건다는 것은 말그대로 그 동작이 실행되는 동안은 아무것도 못한다는것이다. 이는 비효율 적이다.
가시성 + 원자성
그럼 블록을 걸지말고 현재 스레드가 가지고있는 값과 메모리가 가지고 있는 값을 비교하여
자바에서는 CAS알고리즘을 이용한 Atomic Type을 지원한다. 이는 넌 블록 방식이며 블록 방식보다 효율적이다. 이를통해 가시성과 원자성 이슈를 해결할 수 있다.
궁금했던 것들
blocking / non-blocking?
멀티스레드에서 Blocking은 동기화를 위해 현재 스레드의 작업이 완료될때까지 다른 스레드의 작업 시작을 막는 것을 의미한다.
Non- Blocking은 현재 스레드의 작업이 수행과 상관없이 다른 스레드의 작업도 실행되는 것을 의미한다.
가시성 / 원자성?
가시성 : CPU - Cache - Memory 상의 개념
원자성 : “check-then-act” 상황. 즉, 공유되는 변수를 변경할때, 기존의 값을 기반으로 하여 새로운 값이 결정되는 과정에서 여러 스레드가 이를 동시에 수행할 때 생기는 이슈
Stateless Object는 스레드 세이프한가?
Stateless object 는 다른 객체와의 공유 자원을 가지고 있지 않으므로 원자성이 보장된다.
'개발 > 백앤드' 카테고리의 다른 글
[2] [개인공부] 스레드 세이프티(Thread safety) 활용. - synchronized vs cas (2) | 2024.04.19 |
---|---|
[0] [개인 공부] 스레드 세이프티(Thread safety) (0) | 2024.03.26 |
[2] 인X타그램에서 게시물 작성 시 어떤일이 일어날까? - multipart/form-data편 (0) | 2024.03.19 |
[1] 인X타그램에서 게시물 작성 시 어떤일이 일어날까? - multipart/form-data편 (1) | 2024.03.17 |
[MySQL] 레코드 기반의 잠금이란? (0) | 2023.03.05 |