교착상태(Dead-lock) 해결 방법 - Lock Leveling
지난번에 다뤘던 교착상태(dead-lock) 예제를 다시 보겠습니다.
using System;
using System.Threading;
namespace ConsoleApplication2
{
class Program
{
static void Main(string[] args)
{
Program p = new Program();
Thread t1 = new Thread(p.t1);
Thread t2 = new Thread(p.t2);
t1.Name = "lockAB";
t2.Name = "lockBA";
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
object lockA = new object();
object lockB = new object();
// Thread 1
void t1()
{
lock (lockA)
{
Thread.Sleep(2000);
lock (lockB)
{
Console.WriteLine("lockA -> lockB");
}
}
}
// Thread 2
void t2()
{
lock (lockB)
{
Thread.Sleep(2000);
lock (lockA)
{
Console.WriteLine("lockB -> lockA");
}
}
}
}
}
이런 경우에 대표적인 해법으로 lock에 우선 순위를 부여하는 것입니다.
Advanced Techniques To Avoid And Detect Deadlocks In .NET Apps
; https://docs.microsoft.com/en-us/archive/msdn-magazine/2006/april/avoiding-and-detecting-deadlocks-in-net-apps-with-csharp-and-c
예를 들어, lock 변수에 번호를 매겨서 반드시 번호가 작은 것이 외곽에 있어야 한다는 룰을 세우는 것입니다.
object lock1 = new object();
object lock2 = new object();
// Thread 1
void t1()
{
lock (lock1) // 1번 lock이 먼저 잠기고,
{
Thread.Sleep(2000);
lock (lock2) // 2번 lock이 잠겼으므로 적법한 lock 사용
{
Console.WriteLine("lockA -> lockB");
}
}
}
// Thread 2
void t2()
{
lock (lock2) // 2번 lock이 외곽에서 잠겼는데,
{
Thread.Sleep(2000);
lock (lock1) // 번호가 작은 1번 lock을 내부에서 잠그는 것은 규칙 위반
{
Console.WriteLine("lockB -> lockA");
}
}
}
물론, "
Advanced Techniques To Avoid And Detect Deadlocks In .NET Apps" 글에서는 이렇게 실수의 가능성이 있는 규칙에 의존하지 않고 아예 락 객체 생성자에서 번호를 받는 방식으로 구현합니다.
그런데, 사실 번호를 일일이 지정하는 것도 여간 귀찮은 것이 아닙니다. 그래서 아예 다음과 같이 클래스를 만들어 보았습니다.
using System;
using System.Threading;
namespace ConsoleApplication2
{
class Program
{
static void Main(string[] args)
{
Program p = new Program();
Thread t1 = new Thread(p.t1);
Thread t2 = new Thread(p.t2);
t1.Name = "lockAB";
t2.Name = "lockBA";
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
LockLevel lockA = new LockLevel();
LockLevel lockB = new LockLevel();
// Thread 1
void t1()
{
using (lockA.Lock())
{
Thread.Sleep(2000);
using (lockB.Lock())
{
Console.WriteLine("lockA -> lockB");
}
}
}
// Thread 2
void t2()
{
using (lockB.Lock())
{
Thread.Sleep(2000);
using (lockA.Lock())
{
Console.WriteLine("lockB -> lockA");
}
}
}
}
public class LockLevel : IDisposable
{
int _currentLockLevel = 0;
int _oldLockLevel = 0;
object _lockThis = new object();
static int _lockClassLevel = 0;
[ThreadStatic]
static int _checkLockLevel = 0;
bool _locked = false;
public LockLevel()
{
_currentLockLevel = Interlocked.Increment(ref _lockClassLevel);
}
public IDisposable Lock()
{
if (_checkLockLevel > _currentLockLevel)
{
throw new ApplicationException("Deadlock may occur!");
}
_locked = true;
_oldLockLevel = _checkLockLevel;
_checkLockLevel = _currentLockLevel;
Monitor.Enter(_lockThis);
return this;
}
void Free(bool disposing)
{
if (_locked == true)
{
_locked = false;
_checkLockLevel = _oldLockLevel;
Monitor.Exit(_lockThis);
}
}
public void Dispose()
{
Free(true);
GC.SuppressFinalize(this);
}
~LockLevel()
{
Free(false);
}
}
}
위의 예제 코드를 실행하면 "Deadlock may occur!"라는 메시지와 함께 예외가 발생함으로써 교착상태가 발생할 수 있는 가능성이 있음을 알립니다.
아직 현장 검증이 되지 않은 코드이므로 너무 믿지 마시고 ^^ 적절하게 사용하시면 되겠습니다.
(
첨부 파일은 위의 예제 코드를 포함합니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]