[Backend] 쓰레드와 동시성

Updated:

🖥 쓰레드는 무엇일까 ?

[Backend] OS의 프로세스 관리 에서 OS 는 프로세스 단위로 작업을 한다고 했다.

이때 프로세스의 작업을 다시 여러개로 쪼갠 것을 쓰레드 라고 한다.

A 라는 프로세스 안에 각자 맡은 역할이 있는 쓰레드 여러 개가 있는 것 이다.


Single-Thread, 즉 쓰레드가 한 개만 있는 프로세스는 PCB, Data, Program Code, User Stack, Kernel Stack 을 한 개씩 가진다.


하지만 여러 개의 쓰레드를 가진 멀티 쓰레드에서는 다르다.

각자의 쓰레드가 자기만의 TCB, User Stack, Kernel Stack 을 가지며 PCB, Data, Program Code 는 공유한다.

이 쓰레드들도 CPU 가 한 개인 경우엔 무조건 한 개만 실행이 가능하다.

이때도 마찬가지로 아주 빠르게 스위칭을 해 동시에 실행 하는 것 처럼 보이는 것이다.

⚙️ 쓰레드 구성요소

쓰레드는 프로세스의 PCB, Data, Program Code 는 공유하지만 개별적으로 TCB, User Stack, Kernel Stack 을 가지고 있다.

이때 TCBThread Control Block 의 줄임말으로 PCB 의 일부를 저장하고 있다.

TCB 는 쓰레드를 구분하기 위한 쓰레드 ID, 쓰레드를 스위칭 할 때 필요한 정보인 PSI, PCI 중 실행과 관련된 정보를 저장한다.

그렇다면 스택을 따로 가지고 있는 이유는 무엇일까 ?

스택은 함수 내 에서 선언되고 사용되는 지역변수를 저장하는 곳 이다.

이렇게 각자 스택을 가지고 있다는 것은 쓰레드끼리 독립되었고, 독립적인 함수 호출이 가능하고, 독립적인 실행 흐름이 가능하다.

따라서 독립적인 최소 조건을 만족하기 위해 스택을 각자 저장하는 것 이다.

🧰 Multi-Thread 의 장점

쓰레드가 한 개인 Single Thread 보다 Multi-Thread 는 장점 밖에 없다.. 먼저 한마디로 정리하자면 프로세스 단위보다 빠르다.

  1. 새로운 프로세스를 만드는 시간 보다 쓰레드를 만드는 시간이 더 짧다.
  2. 프로세스 종료 시간보다 쓰레드 종료 시간이 더 짧다.
  3. 프로세스 스위칭 시간보다 쓰레드 스위칭 시간이 더 짧다.
  4. OS 없이 쓰레드 간 통신이 가능하다

여기서 4번 장점이 가장 중요하다. 원래 프로세스 끼리 통신을 하려면 커널의 도움을 받아야 했다.

하지만 쓰레드는 애초에 프로세스에 대한 자원을 공유하고 있기 때문에 커널의 도움 없이 서로 통신이 가능하다.

🧲 Multi-Thread 구현

Multi-Thread 를 구현하는 데 있어서 세 가지 방법이 있다.

  1. User-Level Thread

    여기선 Kernel 이 멀티 쓰레드를 지원하지 않아 마치 멀티 쓰레드 인 것 처럼 동작하는 방식이다.

    User Space 에서 쓰레드 라이브러리가 쓰레드를 관리한다.

    때문에 커널은 쓰레드의 존재를 모르며 쓰레드 스위칭을 할 때 커널 모드에서 이루어지지 않는다.

  2. Kernel-Level Thread

    Kernel 에서 멀티 쓰레드를 지원 해 Kernel Space 에서 멀티 쓰레드를 구현한다.

    Kernel 이 프로세스와 쓰레드의 context 를 유지하며 관리한다.

  3. Combined Approach

    말 그대로 User, Kernel-Level 동작 방식을 섞은 것 이다.

    프로그래머가 User Space 에서 불필요하게 과도한 쓰레드를 생성 한 경우

    쓰레드 라이브러리를 통해 줄이고 Kernel 부분에서 적당한 갯수의 쓰레드로 매핑하여 구현한다.

    대표적인 Combined Approach 를 사용하는 OS 로 Solaris 가 있다.

⛓ 동시성

말 그대로 동시에 여러 프로그램을 실행 하는 것이다. 하지만 CPU 가 한 개라면 어떻게 동시성을 만들 수 있을까 ?

지금까지 OS 를 정리하면서 계속 말했던, 아주 빠르게 스위칭을 해 번갈아 가며 실행을 하면 우리 눈에는 동시에 실행 되는 것 처럼 보인다.

하지만 만약 여러 쓰레드가 같은 자원을 사용하는 경우 꼭 동기화 를 해주어야 한다.

해주지 않는다면 다음과 같은 불상사가 일어날 수 있다.

A 라는 쓰레드를 통해 총 10,000원이 있는 나의 계좌에 5,000원을 입금 하였다.

이때 거의 동시에 B 라는 쓰레드를 통해 내 계좌에서 5,000원을 출금 하였다. 결과는 내 계좌에 5,000원이 남아있을 것 이다.

A 가 실행 된 결과를 동기화 시켜주지 않았기 때문에 B 는 내 계좌에 10,000원이 있는 것으로 알고 5,000원을 출금 한 것이다.

이를 해결하기 위해 세마포어, 뮤텍스, 모니터 같은 방법을 사용한다.

아무리 빠르게 번갈아가며 쓰레드를 실행 해도 동기화가 필요한 자원에 접근 할 땐 한 번에 한 쓰레드만 오게 하는 것 이다.

여기서 세마포어 는 이 자원에 동시에 몇 개의 쓰레드가 오게 할래 ? 를 정하는 것 이다.

모니터 는 프로그래밍 언어에서 제공되는 구조체로 세마포어와 동일한 기능을 가지고 있지만 사용하기 쉽다.

계좌와 같이 한 번에 꼭 한 개의 쓰레드만 접근을 해야 하는 경우, 즉 세마포어에서 한 개의 쓰레드만 오게 하는 것을 뮤텍스 라고 한다.

하지만 이 세마포어나 뮤텍스를 잘못 활용하면 Deadlock, Starvation 의 문제가 생길 수 있다.

A 라는 쓰레드가 계좌 정보에 접근 중 일때 B 라는 쓰레드는 A 가 그 데이터를 다 사용 할 때까지 아무것도 안하고 기다린다.

C 라는 쓰레드는 지금 B 가 사용 중인 데이터를 사용 하기 위해 기다리고 있다.

이때 만약 AC 가 사용 중인 데이터를 사용 하기 위해 C 를 기다린다면 어떻게 될까?

서로가 서로를 기다리며 아무것도 못 한채 멈춰 버린다. 이 상태를 Deadlock 이라고 한다.

Starvation 은 해석하면 굶주림의 뜻을 가지고 있다.

A 라는 쓰레드가 어떤 데이터를 사용 중이다. B 라는 쓰레드가 A 를 기다리고 있다.

이때 C 라는 쓰레드도 와서 A 를 기다리고 있는데 B 보다 높은 우선순위를 가지고 있다.

계속 쓰레드가 도착을 하는데 만약 모두 B 보다 높은 우선순위를 가지고 있다면 B 는 작업을 못하고 영영 멈춰 있을 것 이다.

이렇게 자신보다 높은 우선순위를 가진 쓰레드가 계속 와서 나는 아무것도 못할때를 Starvation 이라고 한다.

📕 참조, 출처


Categories:

Updated:

Leave a comment