동시성(Concurrency)이란?

아투·2026년 2월 22일

Operation System

목록 보기
4/22
post-thumbnail

1. 서론

현대 컴퓨팅 환경에서 고성능 시스템을 구축하기 위해서는 자원을 효율적으로 관리하고 활용하는 능력이 필수적이며 특히 멀티 코어 프로세서의 보급과 대규모 트래픽 처리가 일상화된 오늘날 개발자는 동시성과 병렬성의 개념을 명확히 구분하고 이를 제어할 수 있는 기술적 역량을 갖추어야 한다.

본 포스팅에서는 프로세스와 스레드의 근본적인 차이부터 동시성 제어의 다양한 전략 그리고 자바 메모리 모델과 최신 가상 스레드 기술에 이르기까지 견고한 시스템 아키텍처를 설계하기 위한 핵심 이론과 실무 지식을 체계적으로 정리하여 제시한다.


2. 본론

2.1 프로세스 vs 스레드

운영체제에서 프로세스는 실행 중인 프로그램의 독립적인 인스턴스를 의미하며 시스템으로부터 메모리 공간과 자원을 직접 할당받는 가장 큰 실행 단위로서 각 프로세스는 자신만의 코드 영역과 데이터 영역 그리고 힙과 스택 영역을 독립적으로 소유하며 다른 프로세스의 메모리 영역에 접근할 수 없는 보호 구조를 가진다.

(1) 프로세스 메모리 구조

  • Code(Text) Segment

    • 컴파일된 바이너리 코드가 저장되는 영역으로 cpu가 읽어서 실행해야 할 기계어 명령어들이 있음.
    • 이 영역은 Read-Only이자 Fixed-size이다. 실행 중에 로직이 스스로를 수정하면 보안 사고(Self-modifying code)가 날 수 있어서 OS단에서 쓰기를 막아버린다.
    • 예시
      • 장바구니 담기 버튼을 누르면 addToCart() 함수를 실행하라는 기계어 명령문
      • 할인쿠폰 적용 시 결제 금액 계산 로직 (calculataDiscount() )
  • Data Segment

    • 프로그램 시작 시점에 할당되어 종료될 때까지 유지되는 공간.
    • 초기화된 전역 변수(Data)와 초기화되지 않은 전역 변수(BSS)로 나뉜다.
    • 애플리케이션 전체에서 공유되는 설정값(Global Configuration)
    • 여기에 너무 많은 데이터를 때려 넣으면 프로세스가 무거워지고, 여러 스레드가 이 영역의 전역 변수를 동시에 수정하려고 할 때 Race Condition이 발생하는 주 원인이 된다.
    • 예시
      • public static final int MAX_RETRY_COUNT =3; 같은 전역 상수
      • private static DatabaseCennection instace; (싱글톤 객체의 참조 변수/포인터).
  • Heap Segment

    • 엔지니어로서 가장 많이 신경 써야 할 구역이다. new 연산자나 mollac()을 통해 동적으로 생성된 객체들이 저장된다.
    • 모든 스레드가 공유하는 공간이라 멀티스레드 환경에서 동기화 이슈가 여기서 터진다.
    • GC(Garbage Collector)가 이 구역을 감시하며 안 쓰는 객체를 치우지. 만약 참조(Reference)를 끊지 않으면 Memory Leak이 발생해서 서비스가 뻗는다.
    • 예시
      • DB에서 조회해온 List<Product> 인스턴스
      • 사용자의 요청마다 생성되는 UserSession 객체.
      • JSON 파싱 결과물인 Map<String, Object>.
  • Stack Segment

    • 함수 호출 시 생성되는 Stack Frame이 쌓이는 영역.
    • 함수가 끝나면 즉시 사라지는 휘발성 데이터들이다.
    • 각 스레드는 자신만의 독립적인 Stack을 가진다. 그래서 지역 변수는 Thread-Safe한다.
    • LIFO(Last In First Out) 구조라 할당/해제가 엄청나게 빠르지만, 크기가 제한적이라 무한 재귀를 돌면 StackOverflowError를 보게 될 것이다.
    • 예시
      • long orderId, int quantity 같은 함수의 원시 타입(Primitive) 지역 변수.
      • Heap에 있는 객체를 가리키는 참조 변수(Reference) 자체 (예: Order order 변수).
      • 함수 실행 후 돌아갈 Return Address.

(2) 스레드의 공유 영역 vs 독립 영역

동시성 문제가 발생하는 근본적인 원인은 어디까지 같이 쓰고, 어디부터 따로 쓰느냐에 있다.
이 구분만 확실해도 나중에 Thread-Safe라는 개념을 쉽게 이해할 수 있다.

(1) 스레드 메모리의 독립 영역

각 스레드는 실행의 독립성을 유지하기 위해, 최소한의 전용 공간을 가진다.

  • Stack
    • 현재 실행 지점을 기억한다.
    • 함수 호출 시 사용하는 지역 변수매개 변수가 저장된다.
    • 스레드마다 독립적인 스택을 가진다는 것은 함수 호출 흐름을 각자 가져갈 수 있다는 뜻이다.
  • 스택을 따로 사용하는 이유
    • 함수 호출 기록과 로컬 데이터를 기억한다.
    • 만약 스택을 공유한다면, A 스레드가 함수를 호출하는 도중에 B 스레드가 다른 함수를 호출해버리면 리턴 주소가 꼬여서 프로그램이 대혼란에 빠지게 된다.
  • PC Register (프로그램 카운터)
    • 스레드가 명령어를 어디까지 실행했는지 기록하는 지표.
    • CPU가 스레드를 교체하며 실행할 때, 어디부터 다시 시작하면 되는지 알려주는 정보.

(2) 스레드의 공유 영역

스레드들이 프로세스 내에서 서로 소통하고 자원을 아끼기 위해 공동으로 사용하는 영역.

  • Heap : new 키워드 등으로 생성된 동적 객체들이 저장되는 영역. 모든 스레드가 이 객체들에 접근 가능.
  • Data : 전역 변수나 정적(static) 변수가 저장되는 영역. 프로그램 시작부터 종료까지 유지되며 모든 스레드가 공유.
  • Code : 제어문이나 함수같은 실행할 소스코드 자체가 저장된다.

(3) 동시성 문제가 터지는 지점 : 원자성(Atomicity)의 파괴

개발자들이 동시성 프로그래밍에서 고통받는 이유는 바로 공유 영역(Heap, Data) 떄문이다. 여러 스레드가 공유 영역에 있는 하나의 변수를 동시에 수정하려고 할 때, 예상치 못한 값이 나올 수 있다. 그렇다면 왜 Heap과 Data에서만 동시성 문제가 터지는 것인가? 결론부터 이야기하자면, 이 영역들은 여러 명이 동시에 손을 댈 수 있는 공용 공간인데 우리가 하는 작업이 한번에 이루어지지 않기 때문이다.

가장 유명한 예시인 count++(1 증가 시키기) 연산을 생각해 보자. 우리 눈에는 한 줄이지만, CPU는 내부적으로 3단계로 일을 한다.

  • 1단계 : Read

    • 동작 : 메모리(Data/Heap)에 있는 count 값을 읽어온다.
    • 장소 : 공유 영역 -> CPU 레지스터
  • 2단계 : Modify

    • 동작 : 읽어온 값에 1을 더한다.
    • 장소 : CPU 내부
  • 3단계 : Write

    • 동작 : 더한 값을 다시 메모리(Data/Heap)에 저장한다.
    • 장소 : CPU 레지스터 -> 공유 영역
  • 문제가 발생하는 시나리오(Race Condition)
    현제 count가 100이라고 가정하고, 스레드 A와 B가 동시에 1씩 더하려고 한다.

    1. 스레드 A : 100을 읽어간다. (1단계)
    2. 스레드 B : (A가 채 저장하기도 전에) 똑같이 100을 읽어간다. (1단계)
    3. 스레드 A : 100에 1을 더해 101을 메모리에 쓴다. (2-3단계)
    4. 스레드 B : 본인이 읽어왔던 100에 1을 더해 101을 메모리에 쓴다. (2-3단계)
  • 결과 : 102가 되어야하는데, 101이 되어버리는 문제가 발생하게 되는 것이다. 즉, 작업이 마무리가 되지 않은 상태에서 서로 작업을 진행했기 때문에 원자성(Atomicity)이 보장되지 않았다.라는 결과가 나온 것이다.

  • 요약 정리
    공유 영역(Heap, Data)을 사용하는 작업이 원자적(Atomic)이지 않을 떄, 즉 한 스레드의 읽기-수정-쓰기 과정 사이에 다른 스레드가 개입하여 자원을 건드릴 떄 데이터 오염이 발생한다.`

(4) 독립과 공유의 밸런스

스레드는 공유 영역 덕분에 메모리를 아끼고 데이터를 빠르게 주고받을 수 있지만, 그 대가로 데이터 오염이라는 위험을 안고 있다. 반면 독립 영역은 안전하지만 자원이 많이 든다. 결국 동시성 프로그래밍의 핵심은 공유 자원을 얼마나 안전하게 격리하고 관리하느냐에 달려 있다.

(3) Context Switching의 비용

우리가 컴퓨터를 일상적으로 사용할 때 흔히 여러 프로그램이 동시에 돌아가는 것을 보며 CPU가 진짜로 동시에 여러 일을 한다고 생각한다. 하지만, 사실 CPU(코어)는 한번에 하나의 스레드만 실행할 수 있다. 우리가 동시성을 느끼는 건 CPU가 여러 스레드를 인간이 인지할 수 없는 매우 빠른 속도로 교체하며 실행하기 때문이다.

이 교체하는 과정이 바로 컨텍스트 스위칭(Context Switching)이다.

  • 컨텍스트(Context)

    • CPU가 스레드를 실행하기 위해 필요한 현재 진행 상태의 총집합.
      • PC : 어디까지 읽었는지?
      • Registers : 계산 중이던 값은 무엇인지?
      • Stack Pointer : 어디에 저장 중이었는지?
    • 이 모든 기록은 TCB(Thraed Control Block)에 기록된다.
  • 스위칭 과정

    • CPU가 스레드 A를 작업하다가 스레드 B로 넘어갈 때, 다음과 같은 일이 벌어진다.
    1. A의 상태를 저장 : 스레드 A가 쓰던 레지스터 값, PC 등을 A의 TCP에 저장.
    2. 스케줄링 : 다음은 누구 차례인지 OS가 결정.
    3. B의 상태 복구 : 스레드 B의 TCB에서 이전에 저장해둔 상태를 CPU 레지스터로 불러온다.
    4. 실행 : 이어서 B가 중단됐던 지점부터 일을 시작.
  • 컨텍스트 스위칭의 비용(Cost)이 발생하는 이유

    • 직접 비용 : CPU 사이클 낭비
      • 실행 상태를 쓰고 읽는 동안 CPU는 실제 서비스 로직(코드)을 한 줄도 실행하지 못한다. 오로지 관리를 위한 오버헤드(Overhead)만 발생.
    • 간접 비용 : 캐시 오염(Cache Pollution)
      • 가장 큰 원인이다.
      • CPU는 속도를 위해 자주 쓰는 데이터를 캐시(L1, L2 Cache)에 담아둔다.
      • 스레드 A가 작업중에는 캐시에 A의 데이터가 가득차 있다.
      • 갑자기 B로 바뀌면 어떻게 될까? 캐시에 있던 A의 데이터는 쓸모가 없어지고, B는 텅빈 캐시 때문에 느린 메인 메모리(RAM)까지 데이터를 가지러 가야한다. 이를 Cache Miss라고 하며, 성능을 깎는 주범이 된다.
  • 정리
    멀티 스레드는 응답성을 높여주지만, 스레드가 너무 많아지면 실제로 일하는 시간보다 스레드 교체(Context Switching)에 쓰는 시간이 더 많아지는 임계점이 온다. 이를 스레싱(Thrashing) 현상이라고 부른다.


2.2 동시성(Concurrency) vs 병렬성(Parallelism)

1) 동시성

  • 동시성은 여러 개의 작업을 마치 동시에 이루어지는 것처럼 매우 빠르게 교체해 가며 실행하는 논리적인 설계 기법을 의미한다.
  • 동시성은 싱글 코어에서도 가능한 기법이며 물리적인 실행이 아닌 논리적인 설계 기법에 해당한다.
  • 동시성의 주 목적은 대기 시간을 줄여 응답성을 향상시키거나, 자원의 효율을 극대화하는데 포커싱이 되어 있다.

2) 병렬성

  • 병렬성은 동시성과 다르게 실제로 2개 이상의 작업이 물리적으로 실행되는 물리적인 설계 기법에 해당한다.
  • 병렬성은 실제로 물리적으로 실행되기 때문에 싱글 코어가 아닌 멀티 코어에서 가능한 기법이다.
  • 병렬성의 주 목적은 처리량을 극대화하는데 주 포커싱이 되어 있다.

2.3 멀티 스레드 (Multi Thraed)

1) 정의

과거에는 프로세스 안에는 하나의 싱글 스레드만 존재하였고, 프로세스를 실행할 때 하나의 작업만 실행이 가능했다.

가령 예시를 들어, 우리가 웹 브라우저 크롬을 이용한다고 가정할 때, 웹서핑을 하다가, 영화를 다운받으려고 할경우, 그 영화를 다운로드가 완료될 때까지는 크롬은 모든 동작이 멈추게 된다. 왜냐하면 프로세스 안에 싱글 스레드만 존재하기 때문에 2개 이상의 작업을 할 수가 없는 것이다. 이러한 문제를 해결하고자 만들어진 것이 멀티 스레드이다.

2) 멀티 스레드는 동시성과 병렬성 중 무엇이 가능한가?

  • 결론부터 이야기하자면 둘 다 가능하다.

  • 싱글 코어의 경우는 컨텍스트 스위칭을 하여 여러 개의 스레드를 스케줄링에 맞게 교체하여 싱글 코어에서 실행한다.

  • 반면 멀티 코어는 2개 이상의 각각의 코어에서 컨텍스트 스위칭을 사용하여 여러 개의 스레드를 스케줄링에 맞게 교체한다.

  • 다만, 보통의 멀티 코어의 경우는 각 코어에 자기만의 작업 큐를 가지고 있다. 어떤 코어는 가벼운 작업을 만나서 금방 처리되지만, 어떤 코어는 무거운 작업을 연산을 만나 오래 걸릴 수 있다. 이때 노는 코어가 바쁜 코어의 대기 목록 뒷부분을 가져와서 처리하는 방식을 작업 훔치기(Work Stealing)라 하며 운영체제의 부하 분산을 예방한다.

  • 이렇듯 멀티 스레드 환경은 오늘 날 컴퓨터 사용자들에게 있어서 필수불가결적인 기술로 자리잡았다. 그렇다면 멀티 스레드에 대한 트레이드 오프로 문제점이나 단점은 없을까?


2.4 Thraad-Safe

1) 정의

  • 스레드 세이프란 여러 스레드가 동시에 같은 함수, 변수, 혹은 객체에 접근해서 실행하더라도, 프로그램의 실행 결과가 항상 의도한대로 정확하게 나오고 데이터 일관성이 깨지지 않는 상태를 의미한다.

2) 멀티 스레드는 스레드 세이프한가?

  • 안타깝게도 멀티 스레드 환경은 기본적으로 스레드 세이프하지 않는다. 오히려 멀티스레드 프로그래밍에서 가장 큰 숙제가 바로 이 스레드 세이프를 확보하는 것이다. 왜 그런지 메모리 구조와 CPU 동작 원리를 통해 설명해보겠다.

(1) 왜 멀티스레드는 위험한가?

  • 이전에 언급했듯 프로세스 내의 스레드들은 Data, Heap 영역을 공유한다.
  • 각 스레드는 자신만의 독립적인 Stack 영역을 가지므로 지역 변수는 안전하다는 특성이 있다.
  • 하지만, 여러 스레드가 동시에 static 변수나, new 연산자로 만든 객체에 값을 바꾸려 할 때 마다, 문제가 발생하는 것이다.

(2) 스레드 세이프하지 않은 결정적인 원인 1 : 원자성(Atomicity) 문제

  • 앞서 설명한 컨텍스트 스위칭 비용과 같은 이야기이다.
  • 우리가 코드 한 줄로 쓰는 count++는 CPU 입장에서는 3단계를 거치는 작업이다.
    • 1단계 : 메모리에서 count값을 읽어온다. (read)
    • 2단계 : 값에 1을 더한다. (modify)
    • 3단계 : 결과에 다시 메모리를 저장한다. (Write)
  • 만약 스레드 A가 2단계까지 하고 있을 때, 갑자기 컨텍스트 스위칭이 일어나 스레드 B가 값을 읽어버리면 어떻게 될까?
  • 두 스레드는 모두 1을 더 했지만, 결과적으로 값은 2가 아닌 1만 증가하는 데이터 유실이 발생하게 될것이다.

(3) 스레드 세이프하지 않은 결정적인 원인 2 : 가시성(Visibility) 문제

  • CPU는 성능을 위해 메인 메모리에서 매번 데이터를 가져오지 않고, 각 코어의 L1, L2 캐시에 복사해서 사용한다.
  • 스레드 A가 값을 10에서 20으로 바꿨더라도, 그 결과가 메인 메모리에 즉시 반영되지 않고, 캐시에만 머물러 있다면, 다른 코어에서 작동하는 스레드 B는 여전히 여전히 10이라는 옛날 값을 보고 작업을 수행한다.

(4) 스레드 세이프하지 않은 결정적인 원인 3 : 재정렬(Ordering) 문제

  • 컴파일러나 CPU는 성능 최적화를 위해 실행 순서를 임의로 바꿀 때가 있다.
  • 단일 스레드에서는 문제가 없지만, 멀티스레드에는 이 순서가 꼬이면 예상치 못한 결과를 낳는다.

3) 스레드 세이프의 핵심 조건

1) 재진입성(Reentrancy)

  • 어떤 함수가 한 스레드에 의해 실행 중일 때, 다른 스레드가 그 함수를 동시에 호출하더라도 각각 독립적으로 정확한 결과를 내야한다.

2) 상호 배제(Mutual Exclusion)

  • 공유 자원을 사용하는 구간에는 한 번에 하나의 스레드만 접근할 수 있도록 통제해야 한다.

3) 원자적 연산(Atomic Operastions)

  • 데이터 조작이 중간에 끊기지 않고 한 번에 일어나는 것처럼 처리되어야 한다.

4) 가시성(Visbility)

  • 한 스레드에서 변경한 데이터가 다른 스레드에게 즉시 올바르게 보여야 한다.

2.5 스레드 세이프를 구현하는 방법들

1) 뮤텍스(Mutex : Mutual Exclusion)

(1) 정의

  • 뮤텍스(Mutex)는 상호배제(Mutual Exclusion)의 약자로 여러 개의 스레드가 공유 자원(메모리, 파일, 변수 등)에 동시에 접근하는 것을 막아 데이터 오염을 방지하는 동기화 기법이다.

(2) 동작 원리

  • 뮤텍스는 Lock(잠금)Unlock(해제)이라는 두 가지 상태를 가진다.

  • Lock(Acquire)

    • 특정 스레드가 공유 자원을 사용하기 위해 열쇠를 손에 넣는 과정.
    • 이미 다른 스레드가 열쇠를 가지고 있다면 그 스레드가 열쇠를 반납할 때까지 대기한다.
  • 임계 영역(Critical Section)

    • 열쇠를 가진 스레드만이 안전하게 공유 자원을 수정하는 구간.
  • Unlock(Release)

    • 작업이 끝난 스레드가 열쇠를 반납하는 과정.
      이제 대기하던 다른 스레드가 열쇠를 잡을 수 있게 된다.

(3) 뮤텍스의 핵심 특징

  • 뮤텍스가 세마포어와 같은 다른 동기화 도구와 차별화되는 가장 큰 점은 바로 소유권이다.
  • 특정 스레드가 뮤텍스를 잡갔다면, 반드시 그 스레드 본인만 이 잠금을 풀 수 있다.
  • 다른 스레드가 강제로 잠금을 해제하는 것은 불가능하며, 이러한 특성 덕분에 자원관리의 책임이 명확해진다.

2) 세마포어(Semaphore)

(1) 정의

  • 세마포어는 정수 값을 가지는 변수로 표현되며, 주로 두 가지 원자적(Atomic) 연산을 통해 제어된다.

  • 이 연산들은 네덜란드의 컴퓨터 과학자 에츠허르 데이크스트라가 제안한 PV 연산으로 불린다.

  • P 연산(Wait, Down)

    • 자원을 사용하겠다고 알리는 과정.
    • 세마포어 값이 0보다 크면 값을 1 감소 시키고 진행하며, 0이면 자원 생길 때까지 대기(Wait)한다.
  • V 연산(Signal, Up)

    • 자원 사용을 마치고 반납함을 알리는 과정.
    • 세마포어 값을 1 증가시키고, 기다리던 다른 프로세스가 있다면 깨운다.

(2) 세마포어 종류

  • 이진 세마포어(Binary Semaphore)
    • 값이 0 또는 1만 가질 수 있다.
    • 한 번에 하나의 프로세스만 접근해야 하는 상호 배제 구현.
  • 카운팅 세마포어(Counting Semaphore)
    • 값이 0 이상의 정수를 가질 수 있다.
    • 가용한 자원의 개수가 여러 개일 때, 제한된 수의 접근을 허용할 때 사용.

(3) 세마포어를 사용하는 이유

  • 여러 프로세스가 동시에 하나의 데이터를 수정하려고 할 때 발생하는 경쟁 상태를 방지하기 위해 사용된다.

  • 상호 배제 : 한 번에 하나만 자원을 쓰게 해서 데이터가 꼬이는 걸 막는다.

  • 동기화 : 특정 작업이 끝난 뒤에 다음 작업을 시작하도록 순서를 정해줄 수 있다.

  • 자원 관리 : 프린터 3대를 10명이 나눠 써야할 때처럼, 한정된 자원을 효율적으로 배분한다.

3) 모니터(Monitor)

4) 동기화 블록(Synchronized)

5) 동기화 컬렉션(Concurrent Collections)

6) 가시성 보장(Volatile)

7) 스레드 로컬(Thread Local)

8) 트랜잭션 (Transaction)

9) Java.util.concurrent 패키지

10) Aging

11) 원자적 변수(Atomic)

12) 불변성(Immutable)


2.6 동시성 이슈(Concurrency Issues)

  • 경쟁 상태, 기아 상태, 교착 상태 등을 통칭하여 보통 동시성 제어 이슈 또는 멀티스레드 동기화 문제라고 부른다.
  • 좀 더 전문적인 영역이나 학술적인 분류에서는 크게 두 가지 관점으로 나누어 부르기도 한다.
  • 동시성 제어 이슈(Concurrency Control Issues)

    • 가장 보편적으로 쓰이는 말이며, 여러 작업이 동시에 진행되는 환경에서 공유 자원을 안전하게 관리하지 못했을 때 발생하는 모든 문제를 아우르는 표현.
  • 안전성과 활성도 문제

    • 컴퓨터 공학에서는 이 문제들을 성격에 따라 더 정교하게 분류한다.

    • 안전성 문제(Safety)

      • 나쁜 일(데이터 오염)은 절대로 일어나지 않는다는 원칙이 깨진 상태이다.
      • 경쟁 상태가 여기에 해당한다.
      • 데이터가 오염되는 나쁜 일이 발생했기 때문이다.
    • 활성도 문제(Liveness)

      • 좋은 일(작업 완료)은 결국 언제나 일어난다.는 원칙이 깨진 상태이다.
      • 교착 상태와 기아 상태가 여기에 해당한다.
      • 프로그램이 멈추거나 특정 스레드가 일을 못 해 좋은 일이 일어나지 않기 때문이다.
    • 임계 영역 문제(Critical Section Problems)

      • 이 문제들은 모두 임계 영역(Critical Section), 즉 공유 자원에 접근하는 코드 구역을 제대로 보호하지 못하거나 너무 과하게 보호해서 발생한다.
      • 그래서 임계 영역의 3대 결함이라고 부르는 경우도 있다.
  • 결국 이 모든 단어는 멀티스레드라는 고성능 엔진을 다루다가 발생하는 사고들을 뜻한다.

1) 교착 상태(Deadlock)

  • 뮤텍스를 사용할 때 가장 주의해야할 실수는 교착 상태(Deadlock)이다.
  • 스레드 A가 열쇠 1번을 가진 채 2번을 기다리고, 스레드 B는 열쇠 2번을 가진채 1번을 기다리는 상황이 발생하면 프로그램은 영원히 멈춰버릴 수 있다.

(1) 교착 상태(Deadlock) 해결법

  • 데드락 발생의 4가지 필수 조건

    • 상호 배제 (Mutual Exclusion) : 한 번에 한 스레드만 자원을 사용할 수 있음.
    • 점유 및 대기 (Hold and Wait) : 자원을 가진 상태에서 다른 자원을 기다림.
    • 비선점 (No Preemption) : 다른 스레드가 가진 자원을 강제로 뺏을 수 없음.
    • 환형 대기 (Circular Wait) : 스레드들이 원형으로 서로의 자원을 기다림.
  • 예방(Prevention)

    • 데드락 발생 조건 4가지 중 하나를 사전에 차단하는 방식.
    • 자원 공유 허용
      • 상호 배제를 부정하여 여러 스레드가 동시에 자원 쓰게 한다.
      • 현실적으로는 구현이 어려움
    • 자원 일괄 할당
      • 점유 및 대기를 부정하기 위해, 필요한 모든 자원을 한꺼번에 요청하거나 아무것도 가지지 않을 때만 요청하게 한다.
    • 선점 허용
      • 비선점을 부정하여, 다른 자원을 기다려야 할 때 이미 가진 자원을 잠시 내려놓게 하거나 강제로 뺏게 한다.
    • 자원 획득 순서 정하기
      • 원형 대기를 부정하기 위해, 모든 자원에 번호를 매기고 정해진 순서대로만 자원을 요청하게 한다.
      • 가장 많이 쓰이는 방법.
  • 회피(Avoidance)

    • 자원을 할당하기 전에 시스템이 안전한 상태(Safe State)인지를 미리 계산하여 데드락 가능성이 있으면 할당을 거부하는 방식

    • 은행원 알고리즘(Banker's Algorithm)

      • 자원 할당 시마다 시스템이 데드락에 빠질 가능성이 있는지 체크하여, 안전할 때만 자원을 내어주는 방식.
  • 탐지 및 복구(Detection & Recovery)

    • 데드락 발생을 허용하되, 주기적으로 감시하여 발견하면 그때 가서 조치하는 방식
    • 탐지 : 자원 할당 그래프를 통해 사이클이 생겼는지 확인
    • 복구 : 데드락에 빠진 스레드 중 하나를 강제로 종료하거나, 해당 스레드가 점유한 자원을 선점하여 다른 스레드에 준다.
  • 무시(Ignorance)

    • 데드락이 매우 드물게 발생한다면, 이를 해결하는 비용이 더 클 수 있다.
    • 이때는 아무런 조치를 취하지 않고 시스템을 방치하다가 문제가 생기면 사용자가 직접 재부팅하게 한다. 이를 타조 알고리즘이라고 부르기도 한다.

(2) 뮤텍스 이외에도 데드락이 발생할 수 있는 구조

  • 세마포어(Semaphore)
    • 자원의 개수가 1개인 세마포어(이진 세마포어)는 뮤텍스와 동작이 거의 같아 동일하게 교착 상태가 발생.
  • 데이터베이스(DB)
    • 트랜잭션이 A 테이블을 수정(Lock)하고 B 테이블이 수정하려고 하는데, 다른 트랜잭션이 반대로 작업을 수행할 때 발생한다.
  • 모니터(Monitor
    • 자바의 synchronized 메서드끼리 서로를 호출하며 락을 기다릴 때도 발생한다.

2) 기아 상태(Starvation)

  • 기아 상태는 멀티스레드나 멀티 프로세스 환경에서 특정 스레드가 자원(CPU, 메모리, 락 등)을 할당받지 못하고 무한정 대기하게 되는 현상을 말한다.
  • 시스템 전체는 문제없이 돌아가고 있고 다른 스레드들은 활발히 일하고 있는데, 유독 특정 스레드만 운이 없어서(혹은 우선순위가 낮아서) 자기 차례를 영영 보장받지 못하는 소외된 상태라고 이해하면 된다.

(1) 기아 상태 특징

  • 시스템은 정상 작동 중
    • 교착 상태는 모든 스레드가 멈춰버리는 상황이지만, 기아 상태는 일부 스레드만 멈춰있는 상황이 특징이다.
  • 무한 대기
    • 잠시 기다리는 것이 아니라, 이론적으로 영원히 차례가 오지 않을 가능성이 있는 상태.
  • 불공정성
    • 자원 배분 알고리즘이 특정 스레드에게만 불리하게 작용할 때 발생.

(2) 기아 상태 원인

  • 우선 순위 기반 스케줄링의 부작용
    • 운영체제가 우선순위가 높은 스레드부터 처리하라는 규칙을 정했는데, 높은 우선순위의 작업이 끊임없이 계속 들어오면 낮은 우선순위 작업은 영원히 뒤로 밀린다.
  • 비선점형 자원 독점
    • 어떤 스레드가 자원을 한 번 잡으면 아주 오랫동안 놓아주지 않아서, 뒤에 대기하는 스레드들이 굶주리게 된다.
  • 불공정한 락 해제
    • 뮤텍스나 세마포어가 락을 해제할 때 대기 큐에서 가장 오래 기다린 스레드(FIFO)가 아니라, 무작위로 선택하거나 나중에 온 스레드에게 락을 줘버리는 경우에 발생.

(3) 기아 상태(Starvation) 해결법

  • 에이징(Aging)
    • 자원을 기다리는 스레드의 대기 시간이 길어질수록 그 스레드의 우선순위를 점진적으로 높여주는 방식.
    • 처음에는 우선순위가 낮았더라도, 오래 기다리다 보면 결국 최고의 우선순위를 갖게되어 반드시 자원을 할당받게 된다.

3) 경쟁 상태(Race Condition)

  • 경쟁 상태는 앞서 2) 멀티 스레드는 스레드 세이프한가?에서 설명한 대로, 두 개 이상의 스레드가 공유 자원에 동시에 접근하여 값을 수정하려고 할 때, 어떤 스레드가 먼저 실행되느냐(타이밍)에 따라 프로그램의 최종 결과가 달라지는 현상을 말한다.

(1) 경쟁 상태 원인

  • 경쟁 상태는 아래의 세 가지가 모두 충족될 때 발생한다.

    • 공유 자원
      • Heap 영역의 객체나 정적(Static) 변수처럼 여러 스레드가 같이 쓰는 데이터가 있어야 한다.
    • 병행성(Concurrency)
      • 여러 스레드가 거의 동시에 해당 자원에 접근해야한다.
    • 수정(Write)
      • 단순히 읽기만 하는 게 아니라, 최소한 하나 이상의 스레드가 값을 바꾸려고 시도해야 한다.
  • 구체적인 동작 원리는 위에 설명했으므로 생략한다.

(2) 경쟁 상태(Race Condition) 해결법

  • 경쟁 상태를 막으려면, 발생하는 코드 구역인 임계 영역을 동기화 도구로 보호해야 한다.

  • 뮤텍스(Mutex)

    • 자물쇠를 하나만 두어 한 명만 들어오게 한다.
  • 원자적 변수(Atomic)

    • Read-Modify-Write를 쪼개지지 않는 하나의 작업으로 처리한다.
  • 불변성(Immmutable)

    • 아예 값을 바꾸지 못하게(final) 만들어 수정 가능성을 차단.

용어 정리

  • Read-Only

    • 메모리에 한번 로드되면, 그 내용을 절대 수정할 수 없다는 뜻이다.
    • 만약 실행중인 프로그램이 자신의 코드(기계어)영역을 마음대로 바꿀수 있게 된다면 해커가 특정 로직을 가로채서 관리자 권한 부여 같은 엉뚱한 명령어로 바꿔치기할 수 있다.
    • 이를 자기 수정(Slef-modifying code)라고 하는데 현대 OS는 보안상 이유로 이를 엄격히 금지함.
    • CPU는 Code 영역을 읽기만 해야한다. 만약 프로그램이 실수로 이 영역에 데이터를 쓰려고(write) 시도하면, OS는 즉시 Segmentation Fault를 발생 시키고 프로그램을 강제 종료 해버린다. 이는 시스템의 무결성을 지키기 위한 최소한의 안전장치이다.
  • Fixed-size(고정 크기)

    • 프로그램이 실행되는 동안 이 영역의 크기가 절대 변하지 않는다는 뜻
    • 소스 코드를 짜고 컴파일(complie)을 거쳐 빌드가 완료되는 순간, 우리가 실행할 명령어의 총량은 이미 정해져 있다.
    • 실헹 중에 로직(함수나 제어문)이 갑자기 늘어나거나 줄어들 일이 없기 때문이다.
    • 그래서 프로그램이 메모리에 올라갈(Loading) 때, 딱 필요한 만큼의 공간만 할당받고 끝난다.
    • 크기가 고정되어 있다는 것은 관리하기 매우 편하다는 뜻. 메모리의 어디서부터 어디까지가 코드인지 명확하기 때문에, 실행 중에 메모리 위치를 재조정할 필요가 없어 성능 최적화에도 유리하디.
  • malloc()

    • C 언어에서 Heap 영역에 메모리를 동적으로 할당받기 위해 사용하는 핵심 함수.
    • 프로그램 실행 중에 필요한 만큼의 메모리를 운영체제에 요청하는 함수.
    • 컴파일 타임이 아니라, 프로그램이 돌아가는 중(Runtime)에 필요한 크기만큼 메모리를 가져온다.
    • malloc()은 메모리를 빌려주기만 할 뿐, 자동으로 치워주지는 않는다.
    • 사용이 끝난 메모리는 반드시 free(arr);를 호출해서 반납해야 한다.
    • 빌려놓고 free를 안 하면? 그 메모리는 프로세스가 종료될 때까지 힙에 계속 박혀 있게 되고, 결국 Memory Leak이 발생한다.
  • Memory leak(메모리 누수)

    • Memory leak이란 프로그램이 Heap 영역에 메모리를 할당받았지만, 사용이 끝난 후에도 이를 운영체제에 반환하지 않아 메모리 점유율이 계속 높아지는 현상.
  • GC(Garbage Collector)

    • Heap 메모리 내의 미참조 객체를 자동 해제해 누수를 방지하고, 자원 관리를 효율화하는 메모리 관시 시스템이다.
  • Stack Frame

    • 함수 호출 시 스택에 할당되는 공간으로 지역 변수, 매개변수, 반환 주소를 저장하며 종료 시 해제된다.
  • Return Address

    • 함수 종료 후 복귀하여 실행할 다음 명령어의 메모리 주소이며, 함수 호출 시 스택 프레임에 저장된다.
  • StackOverflowError

    • 스택 영역 크기 초과 시 발생하며, 주로 무한 재귀나 과도한 지역 변수 할당이 원인.
  • local context

    • 함수 호출 시 스택 프레임에 생성되는 지역 변수와 매개변수 등 독립적인 실행 환경.

참고 문헌

0개의 댓글