Notice
Recent Posts
Recent Comments
Link
«   2026/05   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Archives
Today
Total
관리 메뉴

I'm FanJae.

[Multi Thread] 2. 호출 스택과 실행 흐름 본문

System Programming

[Multi Thread] 2. 호출 스택과 실행 흐름

FanJae 2026. 4. 26. 15:25

[멀티 스레드 시리즈 글]

[Multi Thread] 1. 멀티 스레드를 이해하기 위한 기초 (프로그램, 프로세스, 스레드)
[Multi Thread] 2. 호출 스택과 실행 흐름
[Multi Thread] 3. 스레드는 어떻게 동작하는가 (컨텍스트 스위칭과 데이터 레이스)

[Multi Thread] 4. 경쟁 상태와 동기화 (원자성, 일관성, 뮤텍스)

[Multi Thread] 5. 교착 상태(Deadlock)와 해결 방법 (잠금 순서, 재귀 뮤텍스)

 

1. 호출 스택(Call Stack)

1) 호출 스택의 기본 개념

프로그램은 많은 서브루틴 혹은 함수로 구성되어 있다.

프로그램이 실행될 때 C/C++에서는 main() 함수가 실행되고,

C#에서는 메인 클래스의 Main() 함수가 실행된다.

 

메인 함수에서 다른 함수를 호출하고, 그 함수가 또 다른 함수를 호출하면서 프로그램의 실행 흐름이 이어진다.

 

함수는 실행이 끝나면 자기 자신을 호출했던 함수의 위치로 돌아가야 한다.

이를 위해 반환 주소, 매개 변수, 지역 변수 등이 스택에 저된다.

이때 함수 호출과 관련된 정보가 쌓이는 공간을 호출 스택(Call Stack)이라고 한다.

 

디버깅 중 중단점을 찍으면 호출 스택 창을 통해서현재 어떤 함수들이 순서대로 호출되어 왔는지 확인할 수 있다.

또한 각 함수 안에서 선언된 지역 변수도 해당 함수의 스택 프레임 안에 저장된다.

#include <iostream>
void func1();
void func2();
void func3();

void func1()
{
	func2();
}
void func2()
{
	func3();
}
void func3()
{
	int a = 0; // 해당 위치에 Debug Break Point를 찍었다.
}
int main(void)
{
	func1();
}

위와 같이 중단점을 찍어보면, 호출 스택에서 어떤 형태로 쌓이는지 확인할 수 있다.

 

각 스레드는 실행 지점이 서로 다를 수 밖에 없다.

스레드를 실행할 때 각 스레드가 최초로 실행할 함수를 지정한다.

서로 다른 함수를 실행하는 스레드라면 당연히 실행 흐름이 달라진다.

 

같은 함수를 실행하더라도 함수에 넘긴 인자, 접근하는 데이터, 실행 시점이 다를 수 있다.

심지어, 같은 함수, 같은 인자, 메모리 상태가 동일해도 실행 지점의 차이가 생길 수 있다.

※ 실행 지점의 차이가 생기는 구체적인 이유는 다음 내용에서 다루고자 한다.

 

따라서 각 스레드는 자기만의 호출 스택을 가진다.

 

2. 스레드의 생성과 소멸

1)  스레드의 생성과 실행 흐름

스레드를 생성하려면 운영체제나 런타임 라이브러리에서 제공하는 스레드 생성용 함수를 호출한다.

이때, 함수 인자로 스레드가 최초로 실행할 함수와 그 함수가 받아들일 매개변수를 넣어줘야 한다.

스레드를 생성하는 함수를 간단화한 형태이다. , 실제 사용 형태와는 다르다.

CreateThread()는 매개 변수로 스레드가 실행할 함수와 그 함수에 전달할 값을 받는다고 생각하면 된다.

이 예시에서는 ThreadProc123을 인자로 넘겼다.

 

이제 메인 스레드는 실행 지점이 로 이동한다.

새로 생성된 스레드 1ThreadProc()를 실행하며 실행 지점이 로 이동한다.

 

이때, 어느 것이 먼저 실행될지는 알 수 없다, 실행 순서가 보장되지 않는다.

실행 순서가 보장되지 않는 이유는 스위칭 개념에서 다시 정리한다.

 

메인 스레드의 가 먼저 끝났다면 메인 스레드는 을 실행할 차례이다.

Join() 함수는 스레드 1의 일이 모두 끝날 때 까지 기다리라는 의미의 함수이다.

 

만약, 스레드 1이 아직 에 머물러 있다면, 메인 스레드는 실행을 일시 정지하고 기다린다.

시간이 지나서 스레드 1가 끝나면, 스레드1은 실행 지점을 으로 옮긴다.

 

ThreadProc()의 실행이 끝나면 스레드 1의 실행도 종료된다.

그 후 Join()에서 기다리고 있던 메인 스레드는 다시 실행을 이어가 로 넘어가서 프로그램을 종료한다.

 

2) 스레드의 일생

스레드를 생성하는 함수를 실행하면 새로운 스레드가 생성되고,

지정한 함수를 실행할 준비를 한다. , 스레드가 시작된다.

 

시작된 스레드는 Runnable 상태가 된다.

Runnable은 실행 가능한 상태를 의미한다.

, CPU를 할당받으면 프로그램의 명령어를 실행할 수 있는 상태이다.

그러다가 다른 무언가를 기다려야 하는 상황이 생기면, 스레드는 Blocked 상태가 된다.

Blocked 상태는 스레드가 잠시 멈춰서 기다리는 상태이다.

 

다른 무언가를 기다려야 하는 Blocked 상태의 구체적인 원인은 이후 동기화나 입출력 개념을 다룰 때 다시 정리한다.

예시 : 파일 I/O, 네트워크 응답 대기, 동기화 등.

 

이후 기다리던 조건이 만족되면 스레드는 다시 Runnable 상태로 돌아간다.

그리고 남은 명령어를 이어서 실행한다.

 

지정된 함수의 실행이 모두 끝나면 스레드의 실행도 종료된다.

이 상태를 Dead 상태라고 볼 수 있다.

 

3) 메인 스레드의 종료와 프로세스의 종료

다른 스레드가 아직 종료되지 않은 상태에서 메인 스레드의 작업이 먼저 끝날 수 있다.

이때 프로세스가 바로 종료되는지는 실행 환경에 따라 다르다.

예를 들어 C++에서 main() 함수가 return되면 프로세스가 종료되면서 다른 스레드도 함께 정리될 수 있다.

 

반면 특정 환경에서는 메인 스레드가 끝나더라도 다른 스레드가 계속 실행 중이면

프로세스가 바로 종료되지 않고 남아 있을 수 있다.

 

따라서 스레드를 만들었다면 보통 join() 등을 통해 스레드가 끝나는 시점을 명확히 맞춰주는 것이 좋다.

 

 

 
 
 
 
 
Comments