개요

스레드thread란 프로세스를 구성하는 실행의 흐름 단위이다. 하나의 프로세스가 여러 개의 스레드를 가지는 것도 가능한데, 이를 멀티 스레드multi thread라고 부른다.

프로세스가 독립적인 메모리 공간을 사용하는 것과 달리 스레드는 프로세스의 자원을 공유한다. 오히려, 스레드끼리 프로세스의 자원을 공유하면서 프로세스 실행 흐름의 일부가 되기 때문에 동시 작업이 가능한 것이다.

메모리 공간

‘스레드끼리 프로세스의 자원을 공유한다’고는 했지만, 그것이 각 스레드가 독립적인 공간을 가지고 있지 않다는 말은 아니다.

스레드 ID, 프로그램 카운터 값을 포함한 레지스터 값과 함께, 독립적인 ‘스택 영역’을 가진다.

독립적인 스택 영역을 가진다는 것은 곧 독립적인 함수 호출이 가능하다는 의미고, 독립적인 함수 호출이 가능하다는 것은 독립적인 실행 흐름이 추가된다는 것이다.

그러므로, 스택 영역을 가짐으로써 스레드는 자신만의 함수 호출 흐름과 지역 데이터를 관리할 수 있으며, 이를 통해 독립적인 실행 흐름을 가질 수 있게 되는 것이다.

반면, 코드 영역, 데이터 영역, 힙 영역은 동일한 프로세스 내의 모든 스레드가 공유한다. 이는 스레드 간 통신과 자원 접근이 빠르다는 장점이 있지만, 동시에 동기화 문제와 경쟁 상태(race condition)를 유발할 수 있다는 단점도 있다.

상태

스레드는 일반적으로 다음과 같은 4가지 상태를 가진다.

상태설명
NEW스레드가 생성되고 아직 호출되지 않은 상태.
RUNNABLE스레드가 실행되기 위해 기다리는 상태.
CPU를 할당받을 수 있는 상태이며, 언제든 실행될 준비가 되어있다.
BLOCKED스레드가 특정 이벤트(입출력 요청 등)가 발생해 대기하는 상태.
CPU를 할당받지 못하며, 이벤트가 발생해 다시 RUNNABLE 상태로 전환될 때까지 대기.
TERMINATED스레드가 실행을 완료하고 종료된 상태.
더 이상 실행될 수 없으며, 메모리에서 제거된다.

스레드 풀

앞서 작성했듯, 하나의 프로세스에서 여러 개의 스레드를 가지는 경우를 멀티 스레드라고 한다.

그러나 스레드를 무한정 생성하는 방식으로 멀티 스레드를 구현하는 것은 문제가 많다. 그 문제들을 해결하기 위한 기술이 바로 스레드 풀이다.

스레드 스케줄링

스레드 스케줄링Thread Scheduling은 운영체제에서 멀티 스레드를 관리하며, CPU를 사용할 수 있는 스레드를 선택하고, CPU를 할당하는 작업을 말한다.

Round Robin, Priority-based scheduling, Multi-level Queue scheduling 등의 스케줄링 알고리즘이 존재한다.

다만 하나의 프로세스 내에서 다수의 스레드가 동작하는 그 특성 상, 스레드 스케줄링은 스레드 간의 상호작용과 동기화 문제를 고려해야 한다.

스레드 컨텍스트 스위칭

프로세스의 컨텍스트 스위칭처럼, 멀티 스레딩 환경에서 스레드 간의 실행을 전환하는 기술이다.

TCBThread Control Block

프로세스의 수행을 재개하기 위해 필요한 데이터들은 PCB에 저장된다. TCB는 이와 비슷하게 스레드에 대한 정보를 담고 있는 자료구조다.

PCB처럼 스레드가 생성될 때 운영체제에 의해 생성되며, 스레드가 소멸될 때 함께 소멸된다.

쉽게 말해 PCB의 스레드 버전이라고 할 수 있겠다.

다만 PCB와의 차이점은, 자원을 공유하는 스레드의 특성 상 스레드 간의 자원 공유와 동기화도 TCB를 통해 관리된다는 점이다.

프로세스 컨텍스트 스위칭과의 비교

1. TCB가 PCB보다 가볍다.

계속해서 언급하는 거지만, 스레드는 프로세스의 메모리(code, data, heap) 영역을 공유한다. 그렇기에 TCB에는 stack 영역 및 레지스터 포인터 정보만을 저장하기 때문에 PCB보다 가벼워 더 빨리 읽고 쓸 수 있다.

2. 캐시 메모리 초기화 여부

프로세스 개념을 되짚어 보자면, 정적 프로그램이 메모리에 적재되고, CPU에 의해 해당 명령어가 실행되는 순간 해당 프로그램은 프로세스가 된다.

즉 프로세스 컨텍스트 스위칭이 일어나면 CPU는 프로그램의 코드를 새로 가져와야 한다. (정확히는, PCB로부터 해당 프로세스에 대한 정보를 가져오는 것이다.)

그렇기에 프로세스 컨텍스트 스위칭이 일어나면 CPU 캐시 메모리를 초기화해야 한다.

반면 스레드 컨텍스트 스위칭은 프로세스 내 스레드 간의 스택과 레지스터 값 등 일부 컨텍스트 정보만 변경되므로 CPU 캐시 메모리는 초기화되지 않는다.

다만 스레드가 다른 CPU 코어에서 실행될 때는 해당 코어의 캐시 메모리에 스레드 컨텍스트 정보가 로드되어야 하므로 초기화될 수도 있다.

경쟁 조건Race Condition

스레드 컨텍스트 스위칭이 발생해 다른 스레드가 heap 영역의 공유 데이터에 접근할 때, 이전 스레드가 이미 공유 자원을 사용하고 있는 경우 동기화 문제가 발생할 수 있다.

예를 들어, 두 개의 스레드가 동시에 하나의 변수를 수정하려고 할 때, 스레드 컨텍스트 스위칭이 발생하면 변수의 값이 잘못된 값으로 업데이트될 수 있는 것이다.

이처럼 여러 스레드가 공유 자원에 동시에 접근하면서, 그 실행 순서나 시점에 따라 프로그램의 결과가 달라지는 오류 상황을 가리켜 ‘경쟁 조건’이라고 한다.

이러한 경쟁 조건은 mutex, semaphore, monitor, atomic operation 등의 동기화 기법을 통해 해결할 수 있다.

뮤텍스mutex

뮤텍스는 오직 하나의 스레드만이 공유 자원에 접근할 수 있도록 함으로써, 바꿔 말해 공유 자원에 대한 동시 접근을 막음으로써 경쟁 조건을 예방하는 기법이다.

스레드가 공유 자원에 접근하기 전에 뮤텍스를 사용해 락Lock을 걸어 다른 스레드가 공유 자원에 접근하지 못하게 한다.

컨텍스트 스위칭이 일어나더라도 다른 스레드는 해당 뮤텍스가 unlock될 때까지 대기하기 때문에, 경쟁 조건을 예방할 수 있다.

예시

공유 변수 countcount 값을 1 증가시키는 thread_A, count 값을 1 감소시키는 thread_B를 가정하자. 이때, 다음과 같은 경쟁 조건이 발생할 수 있다.

count 값 증감 연산은 다음과 같이 이루어진다.

  1. count 값을 읽어온다
  2. 1을 더한다/뺀다
  3. 값을 count에 저장한다.
count = 0;

			thread_A            thread_B
				│
		1. count 값 로드 (= 0)
		2. 1 증가
				│
				└ context switching ─┐
									 │
								1. count 값 로드 (= 0)
								2. 1 감소
								3. count에 저장 (count = -1)
									 │
				┌ context switching ─┘
				│
		3. count에 저장 (count = 1)

thread_Athread_B가 각각 한 번씩 실행되었으므로, count 값은 0이 되어야 함에도 실제로는 1이 저장됨을 볼 수 있다.

참고자료