다중 스레드를 사용하다보면 자원을 공유하게 되는 상황이 있을 수 있다.
이러한 상황에서 일정 영역을 한 번에 한 스레드만 접근 할 수 있는 영역(Critical Section)
으로 만들어 스레드간의 간섭을 없애기 위해 lock문을 사용하고는 한다.
하지만 간혹 lock 영역에서의 작업시간이 길어져 다른 스레드들의 대기 시간이 너무 길어지는 상황이 생길 수 있으며,
스레드간 교착상태인 Dead lock이 발생 할 수도 있으며,
lock 영역에서 외부 라이브러리를 사용하는 경우(왠만해서는 피해야 하지만;) 해당 라이브러리 오류로 반환이 되지 않고
라이브러리 서브루틴에서 무한 루프라도 돌면 해당 모든 스레드가 대기 상태가 되어 프로그램이 맛이 가버리는 골때리는 상황이 오기도 한다.
이럴경우 Monitor.TryEnter (System.Threading.Monitor.TryEnter) 메소드를 사용하여 해결 할 수 있다.
다음 예제는 lock으로 인한 스레드들이 대기 시간을 확인해보자.
작업 시간에 5초가 소요되는 함수를 실행하는 예제이다.
더보기
namespace threadTest
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
Thread t1 = new Thread(() => DoConsoleWirte(1));
Thread t2 = new Thread(() => DoConsoleWirte(2));
Thread t3 = new Thread(() => DoConsoleWirte(3));
Thread t4 = new Thread(() => DoConsoleWirte(4));
t1.IsBackground = true;
t2.IsBackground = true;
t3.IsBackground = true;
t4.IsBackground = true;
t1.Start();
Thread.Sleep(10);
t2.Start();
Thread.Sleep(10);
t3.Start();
Thread.Sleep(10);
t4.Start();
}
static readonly object lockobj = new object();
public void DoConsoleWirte(int id)
{
Console.WriteLine(id + " : START " + DateTime.Now.ToString("HH:mm:ss.fff"));
lock(lockobj)
{
Console.WriteLine(id + " : NOW WORKING " + DateTime.Now.ToString("HH:mm:ss.fff"));
Thread.Sleep(5000); //작업시간 5초(5000 ms)
}
Console.WriteLine(id + " : WORK DONE " + DateTime.Now.ToString("HH:mm:ss.fff"));
}
}
}
실행결과 1 : START 17:16:07.439 1 : NOW WORKING 17:16:07.442 2 : START 17:16:07.452 3 : START 17:16:07.467 4 : START 17:16:07.483 1 : WORK DONE 17:16:12.444 2 : NOW WORKING 17:16:12.444 2 : WORK DONE 17:16:17.446 3 : NOW WORKING 17:16:17.446 3 : WORK DONE 17:16:22.451 4 : NOW WORKING 17:16:22.451 4 : WORK DONE 17:16:27.468 |
t1 - t4 총 4개 스레드가 순차적으로 DoConsoleWirte 함수를 실행 할 때 Critical Section에 접근 할 수 있는 스레드는
한 번에 하나이기 때문에 먼저 실행된 t1 스레드 부터 순차적으로 스레드가 실행 및 종료된다.
위 테스트 에서는 lock문의 동작을 확인하기 위한 것이기에 작업시간을 5초로 설정 하였지만 Critical Section 에서 무한루프에 빠지게 된다면 t2 , t3 , t4 스레드는 무한 대기상태가 된다.
Monitor.TryEnter 함수는 다음과 같이 사용한다.
더보기
bool lockTaken = false; //단독잠금 설정 여부 True 일 때 해당영역에 접근 가능. False 일때는 대기.
Monitor.TryEnter(lockobj, 2000, ref lockTaken); //대기시간 2초(2000ms) 설정
try
{
if (lockTaken)
{
//작업
}
else
{
//시간 초과시 작업
}
}
catch(Exception error) { throw error }
finally { if (lockTaken) Monitor.Exit(lockobj); }
Monitor.TryEnter 를 사용한 예제는 다음과 같다. 작업시간 5초, 대기시간 2초로 설정한 예제이다.
더보기
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
Thread t1 = new Thread(() => DoConsoleWirte(1));
Thread t2 = new Thread(() => DoConsoleWirte(2));
Thread t3 = new Thread(() => DoConsoleWirte(3));
Thread t4 = new Thread(() => DoConsoleWirte(4));
t1.IsBackground = true;
t2.IsBackground = true;
t3.IsBackground = true;
t4.IsBackground = true;
t1.Start();
t2.Start();
t3.Start();
t4.Start();
}
static readonly object lockobj = new object();
public void DoConsoleWirte(int id)
{
Console.WriteLine(id + " : START " + DateTime.Now.ToString("HH:mm:ss.fff"));
bool lockTaken = false; //단독잠금 설정 여부 True 일 때 해당영역에 접근 가능. False 일때는 대기.
Monitor.TryEnter(lockobj, 2000, ref lockTaken); //대기시간 2초(2000ms) 설정
if (lockTaken)
{
Console.WriteLine(id + " : NOW WORKING " + DateTime.Now.ToString("HH:mm:ss.fff"));
Thread.Sleep(5000); //작업시간 5초(5000ms)
}
else
{
Console.WriteLine(id + " : TIME OVER " + DateTime.Now.ToString("HH:mm:ss.fff"));
return;
}
Console.WriteLine(id + " : WORK DONE " + DateTime.Now.ToString("HH:mm:ss.fff"));
}
}
실행결과 1 : START 17:26:37.527 1 : NOW WORKING 17:26:37.530 2 : START 17:26:37.558 3 : START 17:26:37.564 4 : START 17:26:37.582 2 : TIME OVER 17:26:39.569 3 : TIME OVER 17:26:39.569 4 : TIME OVER 17:26:39.599 1 : WORK DONE 17:26:42.531 |
t1 스레드가 작업을 시작하고 t2 , t3 , t4 스레드는 대기상태에 들어간다.
설정된 2초가 지난 뒤에도 t1 스레드가 해당 Critical Section을 점유하고 잠금상태에 있기 때문에
t2 , t3 , t4 스레드는 TIME OVER 메세지를 표시하고 작업을 끝마치는 모습을 볼 수 있다.
위 예제에서는 try catch 문을 사용하지 않았지만, 일부 상황에서 잠금이 해제되지 않을 수 있기 때문에 꼭! 꼮!! try catch finally 사용하시길 바란다.