📒 갈무리 - 스레드(thread)
📌 스레드(thread)란?
- OS가 CPU 시간을 할당하는 기본 단위
- 하나 이상의 스레드로 구성
- 프로세스 내부에서 생성되는 실제로 작업을 하는 주체
- 프로세스 안에 있는 스레드들은 코드, 데이터, 힙 영역은 공유하고 스택은 각각 따로 할당된다.
📌 프로세스(process)란?
- 실행 중인 프로그램(메모리에 적재된 프로그램)
- 스케줄링: 생성 > 준비 > 실행 > 대기 ... > 종료
- 하나의 프로세스는 여러 개의 스레드를 가질 수 있다.
📌 스레드의 장점
- 스레드를 사용하면 이미 프로세스에 할당된 메모리와 자원을 그대로 사용하기 때문에 메모리를 절약할 수 있다.
- 프로그램이 일을 하고 있을 때, 대기할 필요가 없이 다른 일을 처리할 수 있다.
- 프로세스끼리 데이터를 공유할 때, IPC(Inter Process Communication)을 이용해야 하지만, 스레드는 코드 내의 데이터들을 같이 사용할 수 있다.
📌 스레드의 단점
소프트웨어 구현 난이도가 높다.
디버깅 난이도 또한 높다.
하나의 스레드에 문제가 발생하면 전체 프로세스에 영향을 미칠 가능성이 높다.
스레드가 CPU를 사용하기 위해서는 작업 간 전환(Context Switching)을 해야 한다.
📌 동시성 프로세스
- 여러 개의 프로세스가 있을 때, 하나의 프로세스마다 일부분씩 작업을 진행하며 진행 중인 작업을 바꾸는 방식(Context Switching)
이 일이 매우 빠르게 진행되기 때문에 사용자는 마치 동시에 진행되는 것처럼 느끼게 된다.
📌 병렬성 프로세스
- 프로세스 하나에 코어 여러 개가 달려서 각각 동시에 작업을 수행하는 방식(듀얼 코어, 쿼드 코어, 옥타 코어 등 멀티 코어 프로세스가 있는 컴퓨터에서 가능한 방식)
📌 ThreadState
Aborted : 스레드 상태에 AbortRequested가 포함되어 있고 스레드가 작동하지 않지만 상태가 아직 Stopped로 변경되지 않은 상태
AbortRequested : 스레드에 Abort(Object) 메서드가 호출되었지만 해당 스레드는 자신을 종결시키려는 보류된 ThreadAbortException을 받지 못한 상태
Background : 스레드가 백그라운드로 동작되고 있는 상태. Foreground 스레드는 하나라도 실행되고 있는 한 프로세스가 종료되지 않지만, Background는 여러 개가 실행 중이어도 프로세스 상태에 영향을 미치지 않는다. 하지만 프로세스가 종료되면 Background 스레드 또한 모두 종료된다. Thread.IsBackground 속성에 true 값을 입력하면 스레드를 이 상태로 바꿀 수 있다.
Running : 스레드가 시작하여 동작 중인 상태(Unstarted 상태일 때, Thread.Start() 메소드를 통해 Running 상태로 만들 수 있다.)
Stopped : 스레드가 중지된 상태
SuspendRequested : 스레드를 일시 중단하도록 요청하는 상태
Unstarted : 스레드 객체를 생성한 후 Thread.Start()메소드가 호출되기 전의 상태
WaitSleepJoin : 스레드가 차단된 상태(Sleep(), Join(), Enter(), Wait() 등의 함수로 잠금을 허용하거나, ManualResetEvent와 같은 스레드 동기화 개체에서 대기 중인 경우)
📌 스레드 중단하기
Abort
- 함수의 종료를 보장하지 못해, 권장하지 않음
- .NET 5 이상 버전에서는 이 메서드가 사용되지 않고 호출하면 컴파일 시간 경고가 생성됨
interrupt
- 스레드가 동작 중인 상태(Running 상태)를 피해서 WaitJoinSleep 상태에 들어갔을 때 ThreadInterruptedException 예외를 던짐으로 인해 스레드를 중지 시킨다.
- 스레드가 이미 WaitJoinSleep 상태라면 즉시 중단이 가능하지만, 이 상태가 아니라면 스레드를 지켜보고 있다가 WaitJoinSleep 상태가 되었을 때 스레드를 중단 시킨다. 그러므로 WaitJoinSleep 상태가 될 일이 없는 작업을 하고 있을 때에는 중단되지 않는다는 보장을 받을 수 있다.
Join
- 현재 스레드에서 다른 스레드의 실행이 끝나길 기다릴 때 사용
- Join을 활용하여 동기화 작업을 수행할 수 있다.
- 특정 스레드의 작업이 종료되었는지 확인할 수 있다.
📌 스레드 동기화
- 데이터나 변수들을 공유하는 경우
- 여러 개의 스레드가 데이터나 변수들을 공유하게 되면 문제가 발생하게 된다.
예를 들어, 스레드1과 스레드2가 "count라는 변수의 값을 증가시킨다."라고 가정했을 때, count가 0일 때 서로 동시에 가져다가 증가시키게 되면 count를 두 번 증가시킨 것이지만, 실제로는 두 스레드 모두 count의 값이 0일 때 가져갔기 때문에 결국에는 count의 값이 1이 되게 되는 것이다.
이 때문에 스레드들이 질서 있게 자원을 사용할 수 있도록 할 필요가 있다.
크리티컬 섹션(Critical Section)
- 한 번에 한 스레드만 사용할 수 있는 코드 영역
- 스레드의 동기화를 설계할 때는 크리티컬 섹션이 반드시 필요한 곳에만 사용하도록 하는 것이 중요하다. 그렇지 않으면 큰 성능의 하락을 초래하게 된다.
배타적 잠금(Exclusive Lock)
- 공유 데이터를 오직 하나의 스레드만 접근할 수 있다.(lock 키워드, Monitor, Mutex, SpinLock 등을 사용하여 구현)
비배타적 잠금(Non-Exclusive Lock)
- 공유 데이터에 접근할 수 있는 스레드의 개수를 제한할 수 있다.(Semaphore, SemphoreSlim, ReaderWriterLockSlim 등을 사용하여 구현)
lock
- lock 키워드를 사용하여 감싸주면 해당 코드를 크리티컬 섹션으로 만들 수 있다.
- 외부 코드에서도 접근할 수 있는 것은 매개 변수로 사용하지 않는 것이 좋다.(this, string 객체, Type 형식 등)
Monitor
- 스레드 동기화에 사용되는 메소드를 제공
- 하나의 프로세스 안에서만 사용이 가능
Mutex
- Monitor 클래스와 유사한 기능을 제공
- Monitor 클래스는 하나의 프로세스에서만 사용이 가능하지만, Mutex 클래스는 서로 다른 프로세스 간에서도 배타적으로 Locking하는 기능을 가지고 있다.
📌 스레드 예제
스레드 4개를 이용하여 라면을 조리하는 예제
using System;
using System.Threading;
namespace Thread_EX
{
class RamenCook
{
private int ramenCount;
private string[] burners = { "_", "_", "_", "_" };
public RamenCook(int count)
{
ramenCount = count;
}
public void Run()
{
while (ramenCount > 0)
{
lock (this)
{
ramenCount--;
Console.WriteLine(Thread.CurrentThread.Name + " : " + ramenCount + "개 남음");
}
for (int i = 0; i < burners.Length; i++)
{
if (!burners[i].Equals("_")) continue;
lock (this)
{
burners[i] = Thread.CurrentThread.Name;
Console.WriteLine(" " + Thread.CurrentThread.Name + " : [" + (i + 1) + "]번 버너 ON");
ShowBurners();
}
try
{
Thread.Sleep(2000);
}
catch (Exception e)
{
Console.WriteLine("Stack Trace1 : " + e.StackTrace);
}
lock (this)
{
burners[i] = "_";
Console.WriteLine(" " + Thread.CurrentThread.Name + " : [" + (i + 1) + "]번 버너 OFF");
ShowBurners();
}
break;
}
try
{
Random random = new Random();
Thread.Sleep(1000 * random.Next(1, 3));
}
catch (Exception e)
{
Console.WriteLine("Stack Trace2 : " + e.StackTrace);
}
}
}
private void ShowBurners()
{
String stringToPrint = " ";
for (int i = 0; i < burners.Length; i++)
{
stringToPrint += (" " + burners[i]);
}
Console.WriteLine(stringToPrint);
}
}
class Program
{
static void Main(string[] args)
{
try
{
Console.WriteLine("만들 라면의 개수를 입력");
int ramenCount = int.Parse(Console.ReadLine());
RamenCook ramenCook = new RamenCook(ramenCount);
Thread t1 = new Thread(ramenCook.Run);
Thread t2 = new Thread(ramenCook.Run);
Thread t3 = new Thread(ramenCook.Run);
Thread t4 = new Thread(ramenCook.Run);
t1.Name = "A";
t2.Name = "B";
t3.Name = "C";
t4.Name = "D";
t1.Start();
t2.Start();
t3.Start();
t4.Start();
}
catch (Exception e)
{
Console.WriteLine("Exception Message" + e.Message);
}
}
}
}
자바에서는 Runnable interface를 상속받아 Run 메소드를 구현하여 스레드를 동작 시키지만, .NET은 Runnable interface를 지원하지 않기 때문에 delegate를 Thread에 매개 변수에 전달해 줌으로써, Thread를 동작시킬 수 있다.