Thread에 대해서
Process 프로세스
- OS안에서 실행되는 프로그램
- 실행 파일에 담겨있는 데이터 및 코드가 메모리에 적재되어 동작하는 것
프로세스는 반드시 하나 이상의 스레드로 구성된다. 여러 스레드로 구성될 수도 있다.
Thread 스레드
- OS가 CPU에 시간을 할당하는 기본단위
- OS가 명령어를 실행하기 위한 스케줄링 단위
멀티 스레드의 장점
- 동시에 여러 작업을 수행할 수 있다.
- 데이터 공유가 쉽다.
- 어차피 데이터까지 프로세스 안에 들어가기 때문
- 메모리를 절약할 수 있다.
- 하나의 프로세스를 새로 만드는 것보다, 스레드를 만드는게 메모리 사용 측면에서 더 효율적.
멀티 스레드의 단점
- 구현이 복잡하다.
- SW 안전성이 낮아진다.
- 프로세스는 종료될 때 해당 프로세스만 종료되고, 별다른 문제가 없지만
- 스레드는 종료될 때 문제가 생기면, 이를 담고있는 프로세스가 뻑난다.
- 성능이 저하될 수 있다.
- Context Switching의 과정에서 오히려 성능이 저하될 수 있기 때문에, 본래 멀티 스레드를 사용하는 그 의미가 퇴색될 수 있으므로 적절히 사용할 것.
멀티 스레드의 문제
멀티스레드의 문제 1. Race Conditions
레이스 컨디션 (Race Condition)
- 두 개 이상의 스레드가 공유된 자원에 동시에 접근하려고 할 때 발생하는 문제
- 싱글 스레드에서는 이 문제가 발생할 여지가 없다.
멀티스레드의 문제 2. Synchronization Issues
동기화 이슈 (Synchronization Issues)
- 레이스 컨디션을 방지하기 위해 lock을 사용해서 공유 자원을 동기화하는 방식.
멀티스레드의 문제 3. Dead Lock
데드락 (Dead Lock)
- 스레드가 다른 스레드가 가진 락을 무한히 기다려야 할 때 발생한다.
유니티와 스레드
유니티에는 우리가 접근 가능한 하나의 큰 스레드가 존재한다. 이는 메인 스레드로, 유니티의 모든 주요 API는 이 메인 스레드에서만 호출할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestThread : MonoBehaviour
{
void Start()
{
Thread thread = new Thread(PositionCheck);
thread.Start();
}
void PositionCheck()
{
Debug.Log(transform.position);
}
}
이렇게 스레드를 직접 만들어서 동작시키면, 위와 같이 유니티의 기능은 main 스레드에서만 사용 가능함을 알린다.
이 경고는 유니티 엔진의 대부분의 API가 메인 스레드에서만 안전하게 호출될 수 있음을 의미한다.
따라서 유니티에서 병렬 작업이 필요할 때는 코루틴을 사용하는 것이 일반적이다.
코루틴은 메인 스레드에서 실행되며, 여러 작업을 비동기적으로 수행할 수 있게 해준다.
이렇게 함으로써 멀티스레드의 복잡성이나 문제를 피하면서도 동시성을 구현할 수 있다.
멀티 스레드 Multi Thread
유니티에서 스레드를 다루기 - 기초
스레드를 동작시킬 때 데이터를 인자로 넘기기
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
32
33
34
35
36
37
38
39
40
public class TestThread : MonoBehaviour
{
// 스레드에 여러개의 인자를 넘기기 위한 클래스
public class Param
{
public int v1;
public int v2;
}
private Thread thread;
void Start()
{
// 새로운 스레드 만들기 (스레드를 만듦과 동시에 실행할 함수를 연결 => delegate)
thread = new Thread(Temp);
// 인자로 들어갈 객체를 만들고
Param param = new Param();
// 변수 지정
param.v1 = 10000;
param.v2 = 20000;
// 스레드 시작
thread.Start(param);
}
void Temp(object input1)
{
Debug.Log("스레드 시작");
Debug.Log(thread.ThreadState);
Thread.Sleep(2000); // 2초 동안 스레드 중지
// 매개변수 타입 캐스팅
Param param = (Param)input1;
Debug.Log(param.v1 + "|" + param.v2);
Debug.Log("스레드 종료");
}
}
Thread의 Join으로 스레드 동기화 제어
유니티에서 스레드를 사용하는 과정에서 join
을 이용해 스레드 동기화를 제어할 수 있다.
Thread.Join Method (System.Threading)
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
32
33
34
public class TestThread : MonoBehaviour
{
private Thread thread1, thread2;
void Start()
{
thread1 = new Thread(Thread1Function);
thread1.Start();
}
//
void Thread1Function()
{
Debug.Log("Thread 1 시작");
thread2 = new Thread(Thread2Function);
thread2.Start();
thread2.Join(); // thread1은 여기서 멈춘다. thread2가 끝날 때까지
Debug.Log("Join 완료");
}
private void Thread2Function()
{
Debug.Log("Thread 2 시작");
Thread.Sleep(2000); // 2초 동안 스레드 대기
Debug.Log("Thread 2 끝");
}
}
// 실행 결과 :
// Thread 1 시작
// Thread 2 시작
// - 2초 -
// Thread 2 끝
// Join 완료
Join()
함수 : 호출된 스레드가 완료될 때까지, 호출한 스레드를 멈추게 하는 동기화 방법.- 위 코드에서는
thread2.Join();
를 호출해 thread2가 끝날 때까지 thread1이 멈춰있는다.
Thread의 강제 종료 Abort & 방해 Interrupt
Abort
와 Interrupt
는 스레드를 제어하는 두 가지 중요한 메서드.
그러나 Abort
는 위험할 수 있으며, 이를 사용하는 것은 권장되지 않는다.
Thread.Abort Method (System.Threading)
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
32
33
34
35
36
37
38
39
40
41
42
43
public class TestThread : MonoBehaviour
{
private Thread thread1;
void Start()
{
thread1 = new Thread(ThreadFunction);
thread1.Start();
}
void ThreadFunction()
{
try
{
Debug.Log("스레드 시작");
Thread.Sleep(5000); // 5초 대기
Debug.Log("스레드 종료");
}
catch (ThreadAbortException)
{
Debug.Log("스레드 강제 종료");
}
catch (ThreadInterruptedException)
{
Debug.Log("스레드 WaitSleepJoin");
}
}
private void Update()
{
// 일정 시간이 지난 후 스레드를 강제 종료
if (Time.timeSinceLevelLoad > 2f) // 예시로 2초 대기
{
thread1.Abort();
}
}
}
// 실행 결과 :
// "스레드 시작"
// - 2초 -
// "스레드 강제 종료"
Abort() 함수
: ThreadAbortException 예외를 발생시키고 스레드를 종료시킨다. 스레드의 즉시 종료에 가까워 위험할 수 있다.- 대신
Interrupt()
함수를 사용하는 것이 권장된다.thread1.Abort();
대신thread1.Interrupt();
를 사용하면 출력은 다음과 같이 바뀐다.
1
2
3
4
// 실행 결과 :
// "스레드 시작"
// - 2초 -
// "스레드 WaitSleepJoin"
이 방식은 스레드가 WaitSleepJoin
상태가 될 때 종료되도록 합니다.
Background Thread
백그라운드 스레드는 메인 스레드와 달리 프로그램의 실행과 종료에 영향을 미치지 않는 스레드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Program
{
static private Thread subThread;
static void Main(string[] args)
{
subThread = new Thread(SubThreadFunction);
Console.WriteLine("메인 스레드 시작");
subThread.IsBackground = true;
subThread.Start();
Console.WriteLine("메인 스레드 종료");
}
static void SubThreadFunction()
{
Console.WriteLine("서브 스레드 시작");
Thread.Sleep(5000); // 5초 대기
Console.WriteLine("서브 스레드 종료");
Thread.Sleep(3000);
}
}
- 프로그램의 실행과 종료에 영향을 미치지 않는 스레드를 백그라운드 스레드라고 한다.
- 이와 반대로, 기본 스레드는 모두 포그라운드 스레드로 프로그램 실행 종료에 영향을 미친다.
- 위 코드는 하나의 스레드를 만들지만, 이 스레드가 백그라운드 스레드가 되었기 때문에 바로 프로그램은 종료됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Program
{
static private Thread subThread;
static void Main(string[] args)
{
subThread = new Thread(SubThreadFunction);
Console.WriteLine("메인 스레드 시작");
subThread.IsBackground = true;
subThread.Start();
subThread.Join();
Console.WriteLine("메인 스레드 종료");
Thread.Sleep(2000);
}
static void SubThreadFunction()
{
Console.WriteLine("서브 스레드 시작");
Thread.Sleep(5000); // 5초 대기
Console.WriteLine("서브 스레드 종료");
Thread.Sleep(3000);
}
}
- 백그라운드 스레드에
join
을 사용하면 메인 스레드가 멈추며, 백그라운드 스레드가 끝날 때까지 기다린다.- 메인 스레드가 멈추면 프로세스도 멈추므로, 이전과 달리 바로 종료되지 않는다.
스레드의 실행순서
다음의 코드를 유니티에서 실행해보자 (반복문으로 0~9 까지 10개의 스레드를 생성하고 실행시키는 코드)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AboutThread : MonoBehaviour
{
private void Start()
{
for (int i = 0; i < 10; i++)
{
Thread thread = new Thread(TestThread);
thread.Start(i);
}
}
void TestThread(object num)
{
Debug.Log($"{num}번째 스레드");
}
}
실행 결과는 다음과 같다.
그러면 위와 같이, 순서가 뒤죽박죽이 된다. 위와 같이, 스레드의 실행 순서는 예측할 수 없다. 이는 OS가 스레드의 우선순위에 따라 스케줄링을 하기 때문
스레드 상태 Thread State
ThreadState Enum (System.Threading)
ThreadState는 그 이름에서 알 수 있듯, Thread 객체의 상태를 나타내는 정보다.
이는 다음과 같이 System.Threading namespace안에서 enum으로 정의되어있으며, 그 종류는 다음과 같다.
- Running: 스레드가 실행 중인 상태
- StopRequested: 스레드 중지를 요청받은 상태
- SuspendRequested: 스레드 일시중단을 요청받은 상태
- Background: 백그라운드 스레드 상태
- Unstarted: 스레드가 시작되지 않은 상태
- Stopped: 스레드가 완료된 상태
- WaitSleepJoin: 스레드가 일시중단된 상태 (특정 조건을 기다림)
- Suspended: 스레드가 일시중단된 상태
- AbortRequested: 스레드 중지를 요청받은 상태
- Aborted: 스레드가 중지된 상태
ThreadState의 종류
Unstarted
- 스레드가 시작하기 전 상태로, 스레드의
Start()
메서드가 호출되지 않은 상태.
Running
- 스레드가 동작하고 있는 상태로, 스레드가 시작했고 멈추기 전의 상태.
Suspended
- 스레드가 일시 중단된 상태로,
Resume()
함수가 호출될 때까지 대기.
WaitSleepJoin
- 스레드가 특정 조건에서 일시 중단된 상태. 대기 중이며,
Wait
,Sleep
, 또는Join
메서드에 의해 중단된다.
Aborted
- 스레드가 취소된 상태.
ThreadAbortException
예외를 발생시키며,Abort()
함수가 호출되면 스레드가Aborted
상태가 된다. 이 상태에서Stopped
상태로 전환된다.
Background
- 백그라운드 스레드로 실행되고 있는 상태입니다. 프로세스의 수명에 영향을 주지 않는다.
~Requested
- 해당 상태로 전이되기 위해 요청을 받은 상태. 특정 상태로 전이시키는 함수가 호출된 직후의 상태라고 이해할 수 있다.
Interrupt() 함수의 영향
Interrupt()
함수는 호출 즉시 스레드를 중단시키지 않는다.Abort()
함수와 달리,Interrupt()
함수는 스레드가WaitSleepJoin
상태에 들어갈 때 중단된다.
Thread Pool 스레드 풀
스레드 풀은 재사용 가능한 스레드의 집합을 의미한다. 필요할 때마다 스레드를 생성하는 대신, 기존의 스레드를 재사용하여 성능을 최적화할 수 있다.
스레드의 동작방식은 크게 두 가지로 나뉜다.
- 상시 실행 스레드
- 스레드가 비교적 오래 생성되어 있는 방식으로, 주로 무한 루프를 사용한다.
- 일회성 임시 실행 스레드
- 특정 연산만을 수행하고 바로 종료되는 스레드입니다. 스레드가 많이 필요해질 경우 성능을 위해 스레드 풀의 사용이 요구된다.
기초적인 스레드의 시작 방식
1
ThreadPool.QueueUserWorkItem(/*스레드로 시작시킬 함수*/);
매개변수가 있는 경우 스레드의 시작 방식
1
ThreadPool.QueueUserWorkItem(/*스레드로 시작시킬 함수*/, /*함수의 매개변수*/);
매개변수가 여러개 있는 경우 스레드의 시작 방식
여러 값을 저장하는 또 다른 객체 (클래스 혹은 구조체)를 만들어 인스턴스를 인자로 넘길 수 있다.
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
struct MultiParams
{
public int param1;
public int param2;
public int param3;
}
public class AboutThread : MonoBehaviour
{
private void Start()
{
MultiParams params1 = new MultiParams()
{
param1 = 1, param2 = 2, param3 = 3
};
ThreadPool.QueueUserWorkItem(TestThreadPool, params1);
}
void TestThreadPool(object value)
{
MultiParams params1 = (MultiParams)value;
Debug.Log($"첫번째 인자 : {params1.param1}");
Debug.Log($"두번째 인자 : {params1.param2}");
Debug.Log($"세번째 인자 : {params1.param3}");
}
}
스레드 락 Thread Lock
스레드 동기화
스레드 동기화는 여러 스레드가 동시에 공유 자원에 접근할 때 발생하는 문제를 해결하기 위한 방법. C#에서는 lock
키워드를 통해 동기화를 구현할 수 있다.
C#의 Lock
lock
키워드는 다른 스레드가 특정 코드 블록에 접근하는 것을 막아서 자원을 한 번에 하나의 스레드만 사용하도록 보장한다. 이는 크리티컬 섹션 (Critical Section)을 선언해 특정 코드 블록에 대한 동시 접근을 제한하는 방식으로 동기화를 실행한다.
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
public class AboutThread : MonoBehaviour
{
private Thread t1, t2;
private int num;
private void Start()
{
num = 0;
t1 = new Thread(TestFunction);
t2 = new Thread(TestFunction);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Debug.Log(num);
}
void TestFunction()
{
for (int i = 0; i < 1000000; i++)
{
num++;
}
}
}
위 코드를 실행한 결과, 콘솔창에는 예상치 못한 값이 출력된다.
이는 Race Condition으로 인해 발생한다.
레이스 컨디션 Race Condition
Race Condition은 두 개 이상의 스레드가 공유된 자원에 동시에 접근하려고 할 때 발생하는 문제.
위 코드에서는 두 스레드 t1
, t2
가 공유 자원인 num
에 접근을 시도하는 과정에서 Race Condition이 발생한다.
두 스레드가 각각 반복문을 통해 변수 num
의 증감 연산을 1000000회 시도하지만, 이 연산은 원자적 연산이 아니기 때문에 예상치 못한 결과가 발생할 수 있다. 결과값은 2000000이 될 것처럼 보이지만, 실제로는 그렇지 않다.
이를 그림으로 표현하면 위와 같다.
그러면 RaceCondition 이 발생하지 않도록, 위에서 언급한 C#의 Lock을 사용해보자.
먼저 object
타입의 변수를 선언하고, 이를 매개변수로 lock
키워드와 함께 사용한 크리티컬 섹션 (Critical Section) 안에서 증감연산을 수행하도록 해보자.
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
32
33
public class AboutThreadLock : MonoBehaviour
{
private Thread t1, t2;
private int num;
// 동일한 잠금 객체
private readonly object _lock = new object();
private void Start()
{
num = 0;
t1 = new Thread(TestFunction);
t2 = new Thread(TestFunction);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Debug.Log(num);
}
void TestFunction()
{
for (int i = 0; i < 1000000; i++)
{
lock (_lock)
{
num++;
}
}
}
}
이 코드는 매번 실행해도 동일한 결과인 2000000을 출력한다.
이는 lock
키워드를 통해 Race Condition을 방지했기 때문이다.
크리티컬 섹션 (Critical Section)
크리티컬 섹션은 한 번에 한 스레드만 사용할 수 있는 코드 영역을 의미한다.
lock
키워드의 인자는 일반적으로 object
타입을 많이 사용한다. 이는 동일한 인스턴스를 사용해 동일한 락을 보장하기 위함이다.
this
, Type
, string
형식을 lock
인자로 사용하는 것은 권장되지 않는다. 이들은 외부에서 접근할 수 있는 위험성이 있어, 동기화 문제를 일으킬 수 있기 때문이다.
부가 설명
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
public class MyClass1
{
public void MyMethod1() { lock (this) { Debug.Log("MyMethod1"); } }
public void MyMethod2() { lock (this) { Debug.Log("MyMethod2"); } }
public MyClass1 GetInstance() { return this; }
}
public class AboutThreadLock : MonoBehaviour
{
private Thread t1, t2;
private MyClass1 _myClass = new MyClass1();
private void Start()
{
MyClass1 instance = _myClass.GetInstance();
instance.MyMethod1(); // 외부에서 동일한 인스턴스에 접근이 가능해서 문제가 발생한다.
t1 = new Thread(_myClass.MyMethod1);
t2 = new Thread(_myClass.MyMethod2);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
}
위와 같이 Lock의 인자로 this를 사용하면 외부에서도 해당 인스턴스에 접근할 수 있다. (?)
논리적으로 모호해진다. (?)
사실 이 말의 정확한 의미를 이해하지 못했다.
일단은 이러한 방식을 권장하지 않는다는 것만 이해해보자.
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
public class MyClass2
{
public void MyMethod1() { lock (typeof(MyClass2)) { Debug.Log("MyMethod1"); } }
public void MyMethod2() { lock (typeof(MyClass2)) { Debug.Log("MyMethod2"); } }
}
public class AboutThreadLock : MonoBehaviour
{
private Thread t1, t2;
private MyClass2 _myClass = new MyClass2();
private void Start()
{
Type type = typeof(MyClass2);
MyClass2 instance = (MyClass2)Activator.CreateInstance(type);
instance.MyMethod1(); // 외부에서 동일한 인스턴스에 접근이 가능해서 문제가 발생한다.
t1 = new Thread(_myClass.MyMethod1);
t2 = new Thread(_myClass.MyMethod2);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
}
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
32
33
34
35
36
// string을 사용하는 경우는 .NET 런타임이 문자열을 인턴(intern)하기 때문에 권장되지 않는다.
// 문자열 인턴(intern)이란 동일한 내용의 문자열이 메모리에서 동일한 객체를 참조하도록 하는 과정이다.
// 따라서 서로 다른 부분에서 같은 string을 lock으로 사용하게 되면 의도치 않은 동작이 나타날 수 있다.
public class MyClass3
{
public void MyMethod1() { lock ("Key") { Debug.Log("MyMethod1"); } }
public void MyMethod2() { lock ("Key") { Debug.Log("MyMethod2"); } }
}
public class MyClass4
{
public void MyMethod3() { lock ("Key") { Debug.Log("MyMethod3"); } }
}
public class AboutThreadLock : MonoBehaviour
{
private Thread t1, t2;
private MyClass3 _myClass = new MyClass3();
private void Start()
{
MyClass4 myClass4 = new MyClass4();
myClass4.MyMethod3();
t1 = new Thread(_myClass.MyMethod1);
t2 = new Thread(_myClass.MyMethod2);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
}
Type
의 경우와 string
을 사용하는 경우도 마찬가지.
아무튼 이렇게 사용하는 방식은 권장되지 않는다고 한다.
또 다른 방식의 스레드 동기화
Interlocked 클래스
Interlocked 클래스는 원자적 연산 (atomic operation) 방식으로 작동합니다. 이를 사용해 간단한 동기화를 구현할 수 있다.
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
public class AboutThread : MonoBehaviour
{
private Thread t1, t2;
private int num;
private void Start()
{
num = 0;
t1 = new Thread(TestFunction);
t2 = new Thread(TestFunction);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Debug.Log(num);
}
void TestFunction()
{
for (int i = 0; i < 1000000; i++)
{
Interlocked.Increment(ref num);
}
}
}
동기화하고자 하는 스레드 간의 연산이 간단한 연산인 경우에는 Interlocked 클래스를, 그렇지 않은 경우에는 lock 키워드를 사용하자.
Monitor 클래스의 활용
lock 키워드는 내부적으로 Monitor 클래스를 활용하는 방식으로 변환되어 동작한다.
[코드 1]
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
32
33
public class AboutThreadLock : MonoBehaviour
{
private Thread t1, t2;
private int num;
// 동일한 잠금 객체
private readonly object _lock = new object();
private void Start()
{
num = 0;
t1 = new Thread(TestFunction);
t2 = new Thread(TestFunction);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Debug.Log(num);
}
void TestFunction()
{
for (int i = 0; i < 1000000; i++)
{
lock (_lock)
{
num++;
}
}
}
}
[코드 2]
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
32
33
34
35
36
37
38
public class AboutThreadLock : MonoBehaviour
{
private Thread t1, t2;
private int num;
// 동일한 잠금 객체
private readonly object _lock = new object();
private void Start()
{
num = 0;
t1 = new Thread(TestFunction);
t2 = new Thread(TestFunction);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Debug.Log(num);
}
void TestFunction()
{
for (int i = 0; i < 1000000; i++)
{
Monitor.Enter(_lock);
try
{
num++;
}
finally
{
Monitor.Exit(_lock);
}
}
}
}
그래서 [코드 1] 과 같이 lock을 사용한 방식은 내부적으로 [코드 2]와 같이 구현되어 있다.
Monitor.Enter(_lock);
로 객체를 활용해 크리티컬 섹션을 만들고,
Monitor.Exit(_lock);
로 이를 해제한다.
Wait과 Pulse
Monitor
클래스를 사용해 스레드 간의 동기화를 더 세밀하게 다룰 수 있다. 이를 통해 스레드 간의 신호를 주고받을 수 있다.
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
32
33
34
35
36
37
38
39
public class AboutThreadLock : MonoBehaviour
{
private readonly object locker = new object();
private void Start()
{
Thread pingThread = new Thread(Ping);
Thread pongThread = new Thread(Pong);
pingThread.Start();
pongThread.Start();
}
void Ping()
{
lock (locker)
{
for (int i = 0; i < 5; i++)
{
Monitor.Wait(locker); // 대기 및 lock 해제
Debug.Log("Ping");
Monitor.Pulse(locker); // 대기 중인 Pong 스레드에 신호 보내기
}
}
}
void Pong()
{
lock (locker)
{
for (int i = 0; i < 5; i++)
{
Monitor.Wait(locker); // 대기 및 lock 해제
Debug.Log("Pong");
Monitor.Pulse(locker); // 대기 중인 Ping 스레드에 신호 보내기
}
}
}
}
Monitor.Wait()
함수는 스레드를 WaitSleepJoin 상태로 변환시키며 대기하게 한다.Monitor.Pulse()
함수는 대기 중인 스레드를 다시 깨운다.
하지만 위 코드는 실행 시 아무런 결과도 반환하지 않는다.
이는 데드락 (Deadlock) 상태에 빠졌기 때문.
데드락 Deadlock
데드락은 둘 이상의 프로세스나 스레드가 서로 상호작용을 기다리면서 무한히 기다리는 상태.
Wait과 Pulse의 동작
Monitor.Wait
함수를 실행하면, 해당 스레드는 WaitSleepJoin 상태로 변하면서 대기한다.- 그리고 스레드는 WaitingQueue에 들어간다.
Monitor.Pulse
함수는 WaitingQueue에서 대기중인 스레드 하나를 ReadyQueue 로 이동시킨다.- 이후에 OS에 따라서 Lock을 획득하고, 해당 스레드가 진행될 수 있다.
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class AboutThreadLock : MonoBehaviour
{
private readonly object locker = new object();
private bool isPingTurn = true;
private void Start()
{
Thread pingThread = new Thread(Ping);
Thread pongThread = new Thread(Pong);
pingThread.Start();
pongThread.Start();
}
void Ping()
{
lock (locker)
{
for (int i = 0; i < 5; i++)
{
while (!isPingTurn)
{
Monitor.Wait(locker); // ping 차례가 아닐 때
}
Debug.Log("Ping");
isPingTurn = false; // Pong 차례로 변경
Monitor.Pulse(locker); // 대기 중인 Pong 스레드에 신호 보내기
}
}
}
void Pong()
{
lock (locker)
{
for (int i = 0; i < 5; i++)
{
while (isPingTurn)
{
Monitor.Wait(locker); // pong 차례가 아닐 때
}
Debug.Log("Pong");
isPingTurn = true; // Ping 차례로 변경
Monitor.Pulse(locker); // 대기 중인 Ping 스레드에 신호 보내기
}
}
}
}
위 코드의 결과로 두 스레드가 Ping-Pong을 서로 번갈아 수행한다.