
C#을 배우다 보면 언젠가 마주치게 되는 거대한 산이 하나 있죠.
바로 비동기 프로그래밍(Asynchronous Programming)입니다.
우리가 작성한 코드는 결국 컴퓨터 위에서 돌아가게 되는데요,
이때 운영체제는 우리의 프로그램을 어떻게 실행하고 관리할까요?
이번 글에서는 비동기 처리의 바탕이 되는 두 가지 핵심 개념,
프로세스(Process)와 스레드(Thread)에 대해 이야기해 보려 합니다.
프로세스는 쉽게 말해 '실행 중인 프로그램' 그 자체를 의미합니다.
여러분이 PC에서 Chrome브라우저를 켜고, Visual Studio를 실행했다면,
프로세스는 운영체제(Windows, Linux 등)에 의해 생성되고 관리되며,
자원(CPU, 메모리 등)을 할당하여 독립적으로 실행되는 작업 단위입니다.
프로세스는 '하나의 독립된 레스토랑'이라고 생각하면 쉽습니다.
- 독립된 공간: 각 레스토랑(프로세스)은 자신만의 주방(메모리 공간)을 가지고 있어요.
- 자원 소유: 레스토랑이 운영되려면 가스, 전기, 수도 같은 자원이 필요하듯,
프로세스도 실행되기 위해 메모리, CPU 같은 자원을 운영체제로부터 할당받습니다.- 안정성: 옆집 레스토랑 주방에 불이 나도 우리 레스토랑은 안전한 것처럼,
하나의 프로세스에 오류가 발생해도 다른 프로세스에 직접적인 영향을 주지 않습니다.
C#에서는 System.Diagnostics네임스페이스의 Process클래스를 통해
현재 실행 중인 프로세스 정보에 접근할 수 있습니다.
[코드]
using System;
using System.Diagnostics;
Process currentProcess = Process.GetCurrentProcess();
Console.WriteLine($"현재 프로세스 이름: {currentProcess.ProcessName}");
Console.WriteLine($"현재 프로세스 ID: {currentProcess.Id}");
Console.WriteLine($"사용 중인 메모리: {currentProcess.WorkingSet64 / 1024 / 1024} MB");
[실행 결과]
현재 프로세스 이름: 실행 중인 C# 프로그램
현재 프로세스 ID: 15520
사용 중인 메모리: 21 MB
이렇게 각 프로세스는 자신만의 고유한 ID와
독립된 메모리 영역을 가지고 운영체제로부터 관리받습니다.
스레드는 프로세스 안에서 실제로 작업을 수행하는 '일꾼'입니다.
하나의 프로세스 안에는 최소 하나 이상의 '스레드'가 존재합니다.
스레드는 레스토랑(프로세스) 안에서 일하는 '요리사'와 같아요.
- 작업 수행: 요리사가 재료를 다듬고, 불을 사용해 요리를 만드는 것처럼,
스레드가 CPU를 사용해서 코드의 명령어를 하나씩 실행합니다.- 자원 공유: 레스토랑에 있는 여러 명의 요리사들이 같은 주방을 '공유'해서 사용하죠?
하나의 프로세스에 속한 여러 개의 스레드는 그 프로세스의 자원을 공유합니다.- 적은 비용: 요리사를 고용하는 것은 레스토랑을 새로 짓는 것보다 빠르고 저렴합니다.
우리가 사용하는 CPU는 한 번에 하나의 일만 처리할 수 있습니다.
그렇다면 여러 개의 스레드가 동시에 돌아가는 것처럼 보이는 이유는 뭘까요?
컨텍스트 스위칭(Context Switching)
운영체제는 매우 짧은 시간 간격으로 이 스레드, 저 스레드를 번갈아 가며 실행합니다.
그래서 여러 작업이 동시에 실행되는 것처럼 보이게 하는 멀티태스킹이 가능해집니다.
이 과정은 약간의 자원이 소요됩니다. 이것을 오버헤드(Overhead)라고 합니다.
따라서 너무 많은 스레드는 프로그램 성능에 좋지 않은 영향을 줄 수 있습니다.
각 스레드는 독립적인 실행을 위해 스택(Stack), 프로그램 카운터(PC), 레지스터를 갖습니다.
자, 이제 C# 코드로 우리 레스토랑의 요리사들을 직접 만들어 볼까요?
System.Threading네임스페이스의 Thread클래스를 사용하면 직접 만들 수 있습니다.
[코드]
using System;
using System.Threading;
class Program
{
static void Main()
{
// '파스타 요리사' 스레드 생성
Thread pastaChef = new Thread(CookPasta);
// '스테이크 요리사' 스레드 생성
Thread steakChef = new Thread(CookSteak);
Console.WriteLine("레스토랑 오픈! 요리를 시작합니다.");
// 요리사 일 시작!
pastaChef.Start();
steakChef.Start();
// 메인 스레드(매니저)는 자신의 일을 합니다.
for (int i = 0; i <= 5; i++)
{
Thread.Sleep(100);
Console.WriteLine($"매니저: 일하는 중... {i}");
}
Console.WriteLine("매니저: 작업 완료!");
// 메인 스레드(매니저)는 요리사들이 일을 마칠 때까지 기다립니다.
pastaChef.Join();
steakChef.Join();
Console.WriteLine("매니저: 모든 요리가 완료되었습니다!");
}
// 파스타를 만드는 작업
static void CookPasta()
{
Console.WriteLine("파스타 요리사: 면을 삶기 시작합니다.");
Thread.Sleep(3000); // 3초 동안 면을 삶는 중
Console.WriteLine("파스타 요리사: 소스를 만들고 있습니다.");
Thread.Sleep(2000); // 2초 동안 소스 만드는 중
Console.WriteLine("파스타 요리사: 파스타 완성!");
}
// 스테이크를 굽는 작업
static void CookSteak()
{
Console.WriteLine("스테이크 요리사: 고기를 손질합니다.");
Thread.Sleep(1000); // 1초 동안 고기 손질 중
Console.WriteLine("스테이크 요리사: 스테이크를 굽습니다.");
Thread.Sleep(4000); // 4초 동안 굽는 중
Console.WriteLine("스테이크 요리사: 스테이크 완성!");
}
}
[실행 결과]
레스토랑 오픈! 요리를 시작합니다.
파스타 요리사: 면을 삶기 시작합니다.
스테이크 요리사: 고기를 손질합니다.
매니저: 일하는 중... 0
매니저: 일하는 중... 1
매니저: 일하는 중... 2
매니저: 일하는 중... 3
매니저: 일하는 중... 4
매니저: 일하는 중... 5
매니저: 작업 완료!
스테이크 요리사: 스테이크를 굽습니다.
파스타 요리사: 소스를 만들고 있습니다.
파스타 요리사: 파스타 완성!
스테이크 요리사: 스테이크 완성!
매니저: 모든 요리가 완료되었습니다!
위 코드를 실행해 보면, 요리가 동시에 진행되는 것을 볼 수 있을 거예요.
여기서 코드를 실행할 때마다 출력 순서가 조금씩 바뀔 수도 있는데,
그 이유는 스레드에 대한 결정권이 우리가 아닌 운영체제에 있기 때문이죠.
스레드들의 실행 순서는 운영체제의 '스레드 스케줄링' 정책에 따라 달라집니다.
여러 스레드를 동시에 실행할 때, 프로그래머는 실행 순서를 가정해서는 안 됩니다.
앞으로 배울 비동기 프로그래밍은 '스레드'를 효율적으로 다루기 위한 고수준의 기술입니다.
'스레드'에 대한 기본 원리를 아는 것은 비동기 프로그래밍에서 큰 도움이 됩니다.
| 구분 | 프로세스(Process) | 스레드(Thread) |
|---|---|---|
| 비유 | 독립된 레스토랑 | 레스토랑 안의 요리사 |
| 정의 | 실행 중인 프로그램의 인스턴스 | 프로세스 내의 실행 흐름 단위 |
| 메모리 | 독립적인 메모리 공간 사용 | 같은 프로세스 내의 메모리 공유 |
| 생성 비용 | 크다 (운영체제 차원의 자원 할당) | 작다 (프로세스 내에서 생성) |
| 통신 | 비교적 복잡하며 비용이 큼 (IPC 필요) | 비용이 적고 효율적 (메모리 공유) |
| 안정성 | 하나가 죽어도 다른 프로세스는 안전 | 스레드 하나가 오류를 내면 프로세스가 종료될 수 있음 |