스레드(Thread) 란?
- 운영체제가 CPU에 시간을 할당하는 기본 단위이다. (운영체제가 CPU에 일을 시키는 최소 단위)
- 스레드를 설명하기 위해서는 먼저 운영체제와 프로세스가 무엇인지 알아야 한다.
운영체제
- 컴퓨터의 하드웨어와 소프트웨어 자원을 관리하고 컴퓨터 프로그램의 실행을 제어하는 시스템 소프트웨어다.
- 프로세스 관리, 메모리 관리, 파일 시스템 관리, 접근 제어 등의 업무를 담당한다.
- 리눅스, 윈도우, ios등이 있다.
프로세스
- 운영체제가 자원을 할당시키고 관리하는 기본 단위이다.
- 프로그램이 메모리에 로드되어 실행되는 상태이다.
- 운영체제 안에서 돌아가는 프로그램을 담당한다고 생각하면 된다.
- 프로그램 상태, 코드, 제어 블록, 메모리 등으로 구성되어 있다.
- 프로세스는 반드시 하나 이상의 스레드로 구성된다.
우리가 사용하는 컴퓨터에서 CTRL + ALT + DEL 을 입력하면 나오는 작업 관리자 창에서 사용중인 프로세스와 스레드를 확인할 수 있다.
위와 같이 CPU 내에서 몇개의 프로세스가 동작중이고 이 프로세스에서 사용중인 스레드의 합이 실시간으로 갱신된다.
또한 CPU내에서 사용하는 프로세스는 대부분 멀티스레드로 동작한다.
멀티스레드란?
- 하나의 프로세스가 여러개의 스레드를 사용하는것을 의미한다.
- 멀티스레드를 사용하면 자원 효율성이 올라가고 성능이 올라가지만 주의해야 할 점이 있다.
장점
- 동시에 여러 작업을 수행할 수 있다.(병렬적)
- 데이터 공유가 쉬워진다.
- 새로운 프로세스를 만드는 것보다 스레드 여러개를 사용하는 것이 메모리를 더 절약할 수 있다.
- 백그라운드에서 여러 작업을 수행하기에 응답성이 올라간다.
단점
- 프로그램 구현 난이도가 높다. (실행 순서가 정해져 있지 않고 레이스 컨디션*, 데드락* 등 고려해야 할 사항이 많음)
- 소프트웨어 안정성이 떨어진다. (스레드 하나에 문제가 생기면 프로세스 전체에 문제가 생김)
- 성능이 저하될 수 있다. (여러 스레드가 하나에 자원에 동시 접근할때)
멀티스레드에서 고려 할 이슈
1. 레이스 컨디션(Race Condition)
- 레이스 컨디션은 둘 이상의 스레드가 하나의 공유 자원에 접근하려 할 때 발생하는 문제이다.
- 동시성 문제로 발생하며 예상과 다른 결과 값을 도출한다.
예시 코드
using System;
using System.Threading;
class Program
{
private static int counter = 0; // 공유 자원
static void Main()
{
Thread thread1 = new Thread(Increment); // 1번 쓰레드 선언
Thread thread2 = new Thread(Increment); // 2번 쓰레드 선언
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine($"Final counter: {counter}");
}
private static void Increment() // 위의 공유 자원을 천만번 증가시킬 함수
{
for (int i = 0; i < 10000000; i++)
{
counter++;
}
}
}
counter라는 int 변수를 공유 자원으로 설정하고 이를 천만번 증가시키는 함수를 간단하게 구현한다.
그리고 스레드를 2개를 만들어 동시에 카운터를 증가시키면 결과 값이 다음과 같이 나온다.
위와 같이 실행때 마다 다른 결과값이 나온다. 즉 데이터의 무결성이 보장되지 않는다.
위 상황에서 동기화를 사용해 레이스 컨디션을 해결해 보자.
using System;
using System.Threading;
class Program
{
private static int counter = 0; // 공유 자원
private static readonly object lockObject = new object(); // Lock을 사용해 하나의 스레드만 접근 가능하게 만들 객체를 선언
static void Main()
{
Thread thread1 = new Thread(Increment); // 1번 쓰레드 선언
Thread thread2 = new Thread(Increment); // 2번 쓰레드 선언
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine($"Final counter: {counter}");
}
private static void Increment() // 위의 공유 자원을 천만번 증가시킬 함수
{
for (int i = 0; i < 10000000; i++)
{
lock(lockObject) // 한번에 하나의 스레드만 접근 가능
{
counter++;
}
}
}
}
Lock을 사용하면 공유 자원에 하나의 스레드가 접근중일때 다른 스레드는 접근이 불가능하다.
따라서 하나의 스레드 작업이 완료 된 후 다음 스레드가 접근해 순차적으로 작업을 수행해 데이터 안정성이 보장된다.
정상적으로 천만 번 올라가야 할 함수가 두번 실행되어 2천만으로 집계된 모습이다.
2. 데드락(Dead Lock)
- 데드락은 두개 이상의 스레드가 서로 상대가 가진 자원을 기다리면서 무한하게 대기중인 상태이다.
- 서로 상대의 자원의 Lock 해제되는걸 기다리면서 시스템이 정지한다.
데드락 발생 조건
데드락은 1971년 E.G.코프만 교수가 코프만 조건(Coffman conditions)이 충족되어야 발생한다고 정의했으며 아래의 4가지 조건이 모두 만족하는 상태를 데드락 상태로 정의할 수 있다고 한다.
- 상호 배제 (Mutual Exclusion):
- 자원은 한 번에 하나의 프로세스만 사용한다.
- 상호 배제가 없다면 데드락은 발생하지 않는다.
- 뮤텍스(Mutex)* 개념의 어원이다.
뮤텍스(Mutex, Mutual Exclusion) - 여러 스레드가 공유 자원에 동시에 접근하지 못하도록 하기 위해 사용되는 동기화 객체.
위에 사용한 Lock은 단일 프로세스 내에서만 작동하지만 mutex는 여러 프로세스 간에도 동기화가 가능하다.
- 점유와 대기 (Hold and Wait):
- 최소한 하나의 프로세스가 하나의 자원을 점유하고 있으면서, 다른 자원을 추가로 요청하며 대기하고 있는다.
- 비선점 (No Preemption):
- 이미 할당된 자원은 해당 프로세스가 자발적으로 해제할 때까지 가져올 수 없다.
- 순환 대기 (Circular Wait):
- 자원을 기다리는 프로세스들이 원형으로 구성되어, 각 프로세스는 다음 프로세스가 점유하고 있는 자원을 기다리고 있는다.
- 데드락의 근본적인 원인이다.
- 대기 우선 순위를 선정하는 방법으로 보통 데드락을 해결한다.
데드락 발생 예시 코드
using System;
using System.Threading;
class Program
{
private static readonly object lock1 = new object(); // 점유할 자원 1
private static readonly object lock2 = new object(); // 점유할 자원 2
static void Main()
{
Thread thread1 = new Thread(DeadLockThread1Func);
Thread thread2 = new Thread(DeadLockThread2Func);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
}
private static void DeadLockThread1Func()
{
lock (lock1)
{
Console.WriteLine("Thread 1: 1번 스레드 lock 1 첫번째 작동");
Thread.Sleep(1000); // 데드락을 시뮬레이션하기 위해 일시 중지
lock (lock2)
{
Console.WriteLine("Thread 1: 1번 스레드 lock 2 두번째 작동");
}
}
}
private static void DeadLockThread2Func()
{
lock (lock2)
{
Console.WriteLine("Thread 2: 2번 스레드 lock 2 첫번째 작동");
Thread.Sleep(1000); // 데드락을 시뮬레이션하기 위해 일시 중지
lock (lock1)
{
Console.WriteLine("Thread 2: 2번 스레드 lock 1 두번째 작동");
}
}
}
}
자원 1번과 2번을 객체로 선언하고 이를 호출할 스레드 2개도 선언해준다.
1번 스레드가 1번 자원을 선점하고 2번 스레드는 2번 자원을 선점한다.
1초 후에 1번 스레드는 2번 자원을 선점하려고 대기 중인 상태이고,
2번 스레드 역시 1번 자원을 선점하려고 대기 중인 상태에서 교착 상태가 발생한다.
위의 상태에서 더 이상 프로그램이 동작하지 않고 무한 대기 상태로 빠진다.
위 데드락을 해결하려면 Lock이 잠기는 순서를 변경해보면 된다.
private static void DeadLockThread1Func()
{
lock (lock1)
{
Console.WriteLine("Thread 1: 1번 스레드 lock 1 첫번째 작동");
Thread.Sleep(1000); // 데드락을 시뮬레이션하기 위해 일시 중지
lock (lock2)
{
Console.WriteLine("Thread 1: 1번 스레드 lock 2 두번째 작동");
}
}
}
private static void DeadLockThread2Func()
{
lock (lock1)
{
Console.WriteLine("Thread 2: 2번 스레드 lock 1 첫번째 작동");
Thread.Sleep(1000); // 데드락을 시뮬레이션하기 위해 일시 중지
lock (lock2)
{
Console.WriteLine("Thread 2: 2번 스레드 lock 2 두번째 작동");
}
}
}
2번 쓰레드 함수 에서도 lock1의 자원을 먼저 점유하면 (자원을 1번 쓰레드와 동일한 순서로 Lock 하면) 데드락이 발생하지 않는다.
정리
- 스레드는 프로세스 안에서 기능이 작동하는 실행 단위이다.
- 운영체제는 컴퓨터 프로그램의 실행을 제어하고 관리하는 시스템 소프트웨어다.
- 프로세스는 운영체제 안에서 작동하는 프로그램이며 메모리에 로드되어 있어야 한다.
- 멀티 스레드는 하나의 프로세스가 여러개의 스레드를 사용하는 것이며 장단점이 있다.
- 멀티 스레드를 사용할 때는 레이스 컨디션과 데드락과 같은 문제를 조심해야 한다.
데드락 사진 출처 : https://pubul.tistory.com/36
15.교착 상태 한컷 요약
교착상태:프로세스가 자기꺼는 안주고 남꺼만 원하기 떄문에 일어나는 현상.참고: http://terms.naver.com/entry.nhn?docId=820185&cid=42344&categoryId=42344
pubul.tistory.com
'C#' 카테고리의 다른 글
C# - 라이브러리 vs 프레임워크 (2) | 2024.07.11 |
---|---|
C# - Struct 와 Class의 차이점 (0) | 2024.06.21 |
C# - 동기와 비동기 (Synchronous/Asynchronous) (0) | 2024.06.13 |
C# - System / Collections / Generic Collection (0) | 2024.06.11 |
C# - 가비지 컬렉션(Garbage Collection) (0) | 2024.06.07 |