한 메서드를 다수의 스레드가 동시에 실행하고 그 메서드에서 클래스 객체의 필드들을 읽거나 쓸때, 다수의 스레드가 동시에 필드값들을 변경할수 있게 됩니다. 객체의 필드값은 모든 스레드가 자유롭게 엑세스할 수 있기 때문에, 메서드 실행 결과가 잘못될 가능성이 큽니다. 이렇게 스레드들이 공유된 자원을 동시에 접근하는것을 박고, 각 스레드들이 순차적,제한적으로 접근하도록 하는것을 스레드 동기화(Thread Synchronization)이라고 합니다. 또한 이렇게 ㅅ레드 동기화를 구현한 메서드나 클래스를 Thread-Safe하다 라고 합니다. .NET의 많은 클래스들은 Thread-Safe하지 않은데, Thread-Safe를 구현하려면 Locking 오버헤드 보다 많은 코딩을 요구합니다. 실제 실무에서 이러한 Thread-Safe를 필요로 하지 않은 경우가 더 많기 때문입니다.
스레드 동기화를 위해서 .NET에는 많은 클래스와 메서드들이 있습니다. 이중 중요한 것들로는Monitor,Mutex,Semaphore,SpinLock,ReaderWriterLock,AutoResetEvent등이 있으며, C#키워드로는 lock, await등이 있습니다. 스레드 동기화를 위해 자주 사용되는 방식으로서, Locking으로 공유 리소스에 대한 접근을 제한하는 방식으로 C# lock,Monitor,Mutex,Semaphore,SpinLock,ReaderWriterLock 등이 사용되며, 타 스레드에 신호를 보내 스레드 흐름을 제어하는 방식으로 AutoResetEvent,ManualResetEvent,CountdownEvent등이 있습니다.
아래 예제는 여러 개의 스레드가 Thread-Safe하지 않은 메서드를 호출하는 예를 보여주고 있습니다. 10개의 스레드가 counter라는 필드를 동시에 쓰거나 읽는 샘플로서 한 스레드가 counter변수를 변경하고 있기 전에 다른 스레드가 다시 counter변수를 변경할 수 있기 때문에 불확실한 결과가 출력되게 될겁니다.
using System;
using System.Threading;
namespace George
{
class MyClass
{
private int counter = 0;
public void Run()
{
// 10개의 스레드가 동일 메서드 실행
for (int i = 0; i < 10; i++)
{
new Thread(UnsafeCalc).Start();
}
}
// Thread-Safe하지 않은 메서드
private void UnsafeCalc()
{
// 객체 필드를 모든 스레드가 자유롭게 변경
counter++;
// 가정 : 다른 복잡한 일을 한다
for (int i = 0; i < 10000; i++)
for (int j = 0; j < 10000; j++);
// 필드값 읽기
Console.WriteLine(counter);
}
}
}
/* 출력 결과
5
5
7
6
7
8
9
10
10
10
*/
C#의 lock 키워드는 특정 블럭의 코드를 임계영역으로 만들어 한번에 하나의 스레드만 실행할 수 있도록 해줍니다. lock()파라미터에는 임의의객체를 사용할 수 있는데 , 주로 object 타입의 private필드를 지정합니다. lock(this)와 같이 클래스 객체 전체를 지정하는 this를 사용할 수도 있는데, 이는 불필요하게 모든 클래스 객체를 잠그는 효과가 있어, object타입의 필드를 만들어 사용하는것이 좋습니다. 임계영역 코드블럭은 가능한한 범위를 작게 하는 것이 좋은데, 필요한 부부만 Locking 한다는 스레드 동기화 설계 원칙에 의거한 것입니다.
using System;
using System.Threading;
namespace George
{
class MyClass
{
private int counter = 0;
// lock문에 사용될 객체
private object lockObject = new object();
public void Run()
{
// 10개의 스레드가 동일 메서드 실행
for (int i = 0; i < 10; i++)
{
new Thread(SafeCalc).Start();
}
}
// Thread-Safe 메서드
private void SafeCalc()
{
// 한번에 한 스레드만 lock블럭 실행
lock (lockObject)
{
// 필드값 변경
counter++;
// 가정 : 다른 복잡한 일을 한다
for (int i = 0; i < 10000; i++)
for (int j = 0; j < 10000; j++);
// 필드값 읽기
Console.WriteLine(counter);
}
}
}
}
//출력 예:
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
// 10
Monitor클래스는 C#의 lock과 같이 특정 코드 블럭을 임계영역으로 만들어 배타적으로 Locking하는 기능을 가지고 있습니다. Monitor.Enter()메서드는 임계영역 블럭을 시작하여 한 스레드만 블럭으로 들어가게 하며, Monitor.Exit()는 Locking을 해제하여 다음 스레드가 임계영역 블럭을 실행하게 합니다. C# lock키워드는 실제로는 Monitor.Enter()와 Monitor.Exit()를 사용하여 임계영역을 try finally문으로 감싼 문장들로 컴파일시 코드를 변경하는 것입니다. 즉, 자주 사용되는 Monitor.Enter() ~ Exit()를 간략하게 표현한것이 lock 키워드입니다.
class MyClass
{
private int counter = 0;
// lock문에 사용될 객체
private object lockObject = new object();
public void Run()
{
// 10개의 스레드가 동일 메서드 실행
for (int i = 0; i < 10; i++)
{
new Thread(SafeCalc).Start();
}
}
// Thread-Safe하지 않은 메서드
private void SafeCalc()
{
// 한번에 한 스레드만 lock블럭 실행
Monitor.Enter(lockObject);
try
{
// 필드값 변경
counter++;
// 가정 : 다른 복잡한 일을 한다
for (int i = 0; i < 10000; i++)
for (int j = 0; j < 10000; j++);
// 필드값 읽기
Console.WriteLine(counter);
}
finally
{
Monitor.Exit(lockObject);
}
}
}
Monitor클래스의 또 다른 중요한 메서드로는 Wait()와 Pulse() / PulseAll()이 있습니다. Wait()메서드는 현재 스레드를 잠시 중지하고 lock을 해제한 후, 다른 스레드로부터 Pulse신호가 올때까지 기다립니다. Wait에서 Lock이 해제되었으므로 다른 스레드가 lock을 획득하고 작업을 실행할수 있습니다. 다른 스레드가 자신의 작업을 마치고 Pulse()메서드를 호출하면 대기중인 스레드는 lock을 획득하고 계속 작업을 실행하게 됩니다. Pulse()메서드가 호출되었을때, 만약 대기중인 스레드가 있다면 , 그 스레드가 계속 실행하게 되지만 만약 대기중인 스레드가 없다면 Pulse신호는 없어집니다. 이러한 Wait/Pulse는 개념적으로 AutoResetEvent과 같은 이벤트 개념과 비슷하다 하지만, AutoResetEvent는 Set()메서드로 펄스신호를 보내는데 대기중인 스레드가 없는 경우 하나의 Pulse 신호가 있었다는 것을 계속 가지고 있게 됩니다. Pulse() 메서드는 다음 한개의 스레드만 계속 실행하게 하지만 PulseAll()메서드는 현대 대기중인 모든 스레드를 풀어 실핼하게 됩니다. Monitor 클래스의 Wait(), Pulse() 메서드를 사용하기 위해 한가지 전제조건이 있는데 이 메서드들이 lock블럭 안에서 호출되어야 한다는 것입니다. 아래 예제는 10개의 작업 스레드들이 데이타를 Queue에 집어넣고, 하나의 일기 스레드가 데이타를 계속 Queue에서 꺼내오는 샘플입니다.
class Program
{
static Queue Q = new Queue();
static object lockObj = new object();
static bool running = true;
static void Main(string[] args)
{
// reader 스레드 시작
Thread reader = new Thread(ReadQueue);
reader.Start();
// writer 스레드들 시작
List<Thread> thrds = new List<Thread>();
for (int i = 0; i < 10; i++)
{
var t = new Thread(new ParameterizedThreadStart(WriteQueue));
t.Start(i);
thrds.Add(t);
}
// 모든 writer가 종료될 때까지 대기
thrds.ForEach(p => p.Join());
// reader 종료
running = false;
}
static void WriteQueue(object val)
{
lock (lockObj)
{
Q.Enqueue(val);
Console.WriteLine("W:{0}", val);
Monitor.Pulse(lockObj);
}
}
static void ReadQueue()
{
while (running)
{
lock (lockObj)
{
while (Q.Count == 0)
{
Monitor.Wait(lockObj);
}
for (int i = 0; i < Q.Count; i++)
{
int val = (int)Q.Dequeue();
Console.WriteLine("R:{0}", val);
}
}
}
}
}
using System;
using System.Threading;
using System.Collections.Generic;
namespace George
{
class Program
{
static void Main(string[] args)
{
// 2개의 스레드 실행
Thread t1 = new Thread(() => MyClass.AddList(10));
Thread t2 = new Thread(() => MyClass.AddList(20));
t1.Start();
t2.Start();
// 2개의 스레드 실행완료까지 대기
t1.Join();
t2.Join();
// 메인스레드에서 뮤텍스 사용
using (Mutex m = new Mutex(false, "MutexName1"))
{
// 뮤텍스를 취득하기 위해 10 ms 대기
if (m.WaitOne(10))
{
// 뮤텍스 취득후 MyList 사용
MyClass.MyList.Add(30);
}
else
{
Console.WriteLine("Cannot acquire mutex");
}
}
MyClass.ShowList();
}
}
public class MyClass
{
// MutexName1 이라는 뮤텍스 생성
private static Mutex mtx = new Mutex(false, "MutexName1");
// 데이타 멤버
public static List<int> MyList = new List<int>();
// 데이타를 리스트에 추가
public static void AddList(int val)
{
// 먼저 뮤텍스를 취득할 때까지 대기
mtx.WaitOne();
// 뮤텍스 취득후 실행 블럭
MyList.Add(val);
// 뮤텍스 해제
mtx.ReleaseMutex();
}
// 리스트 출력
public static void ShowList()
{
MyList.ForEach(p => Console.WriteLine(p));
}
}
}
Mutex 활용에 적합한 예로서 흔히 한 머신 내에서 오직 한 응용프로그램만이 실행되도록 하는 테크닉을 들수 있습니다. 한 컴퓨터 내 한 프로세스만 실행하고자 하기 위해 고유의 Mutex명을 지정합니다. 일반적으로 이렇게 구현하기 위해 GUID를 많이 사용합니다. 처음 프로세스가 먼저 Mutex를 획득하면 다른 프로세스는 Mutex를 획득할 수 없기 때문에 오직 하나의 프로그램만 머신 내에서 실행 되는 것입니다.
class Program
{
static void Main()
{
// Unique한 뮤텍스명을 위해 주로 GUID를 사용한다.
string mtxName = "60C3D9CA-5957-41B2-9B6D-419DC9BE77DF";
// 뮤텍스명으로 뮤텍스 객체 생성
// 만약 뮤텍스를 얻으면, createdNew = true
bool createdNew;
Mutex mtx = new Mutex(true, mtxName, out createdNew);
// 뮤텍스를 얻지 못하면 에러
if (!createdNew)
{
Console.WriteLine("에러: 프로그램 이미 실행중");
return;
}
// 성공하면 본 프로그램 실행
MyApp.Launch();
}
}
using System;
using System.Runtime.ComplierServices;
// 이 네임스페이스를 추가해야 메소드 속성을 부여 할수 있다.
using System.Threading;
namespace George
{
class MyClass
{
private int counter = 0;
// lock문에 사용될 객체
private object lockObject = new object();
public void Run()
{
// 10개의 스레드가 동일 메서드 실행
for (int i = 0; i < 10; i++)
{
new Thread(SafeCalc).Start();
}
}
// Thread-Safe 메서드
[MethodImpl(MethodImplOptions.Synchronized)]
// 메서드 속성옵션으로 스레드 동기화 부여
private void SafeCalc()
{
// 필드값 변경
counter++;
// 가정 : 다른 복잡한 일을 한다
for (int i = 0; i < 10000; i++)
for (int j = 0; j < 10000; j++);
// 필드값 읽기
Console.WriteLine(counter);
}
//출력 예:
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
// 10
}
}
SemaPhore 클래스는 공유된 리소스를 지정된 수의 스레드만 엑세스할 수 있게 허용하는데, 예를 들어 10개의 스레드들이 엑세스 하도록 허용하였다면 11번째 스레드는 현재 사용중인 10개의 스레드중 누군가 리소스 사용을 마쳐야지만 해당 리소스에 진입할 수 있습니다.
using System;
using System.Threading;
namespace George
{
class Program
{
static void Main()
{
MyClass c = new MyClass();
// 10개 스레드들 실행
// 처음 5개만 먼저 실행되고 하나씩 해제와 함께
// 실행될 것임.
for (int i = 1; i <= 10; i++)
{
new Thread(c.Run).Start(i);
}
}
}
class MyClass
{
private Semaphore sema;
public MyClass()
{
// 5개의 스레드만 허용
sema = new Semaphore(5, 5);
}
public void Run(object seq)
{
// 스레드가 가진 데이타(일련번호)
Console.WriteLine(seq);
// 최대 5개 스레드만 아래 문장 실행
sema.WaitOne();
Console.WriteLine("Running#" + seq);
Thread.Sleep(500);
// Semaphore 1개 해체.
// 이후 다음 스레드 WaitOne()에서 진입 가능
sema.Release();
}
}}
/*출력 결과
1
2
3
Running#3
Running#1
Running#2
5
Running#5
6
Running#6
4
7
8
9
10
Running#9
Running#4
Running#10
Running#8
Running#7
*/
[C#] Form 폼 최소화버튼 막기 (0) | 2023.07.25 |
---|---|
[C#] exe 실행파일에 DLL파일을 임베디드하여 배포 하기 (0) | 2023.07.25 |
[C#] 일반화 프로그래밍 (0) | 2023.07.25 |
[C#] 컬렉션 Collection (0) | 2023.07.25 |
[C#] 프로퍼티 (0) | 2023.07.25 |