C# 낮은 수준의 동기화 SpinLock

http://msdn.microsoft.com/ko-kr/library/dd997366.aspx


 우리가 흔히 사용하는 Lock 객체와 같은 일을 하는 클래스가 .Net Framework 4에서 새로 나왔다. SpinLock 객체가 하는 역할은 Lock과 같은 역할을 하지만 새로 나온 이유가 있을 것이다. MSDN에서는 아래와 같이 설명하고 있다.


대기 시간이 짧을 것으로 예상되고 경합이 적으면 다른 종류의 잠금보다 SpinLock을 수행하는 것이 효과적입니다. 그러나 SpinLock은 System.Threading.Monitor 메서드 또는 Interlocked 메서드로 인해 프로그램 성능이 크게 떨어진다는 점이 프로파일링을 통해 확인될 때만 사용하는 것이 좋습니다. 
http://msdn.microsoft.com/ko-kr/library/dd997366.aspx

[표1] SpinLock MSDN 설명 발췌


 위 글에서 Monitor은 Lock이 컴파일이 되면 내부적으로 Monitor로 대체되기 때문에 Lock을 따로 언급하지 않은것으로 보인다. 그렇다면 위 이야기대로라면 SpinLock이 모든 lock 상황에서 성능 우위를 지원하는 것은 아님을 알수 있으니 개발하는 입장에서는 일일이 확인해 보고 적용을 하라는 뜻이다. ^^;;

그래도 SpinLock를 사용해야 하는 상황이 특출나게 성능을 요하는 작업에 대해서만 고려하면 되기 때문에 그나마 다행이라 할 수 있겠다.


 이제 본격적으로 검토를 해 보자. SpinLock와 Lock(Monitor)을 사용해서 성능 비교를 해 보도록 하자.

// 잠김 횟수
const int N = 100000;
static Queue<LockDataObject> _spinLockQueue = new Queue<LockDataObject>();
static Queue<LockDataObject> _lockQueue = new Queue<LockDataObject>();
 
// 잠김에 사용할 객체
static object _lock = new Object();
// 잠김에 사용할 SpinLock 객체
static SpinLock _spinlock = new SpinLock();
 
// 수행에 필요한 데이터 객체
class LockDataObject
{
    public string Name { getset; }
    public double Number { getset; }
}
 
/// <summary>
/// Lock 객체로 잠금 상태에서 Queue에 넣기
/// </summary>
/// <param name="d"></param>
/// <param name="i"></param>
private static void UpdateWithSpinLock(LockDataObject dint i)
{
    // false로 세팅하고 spinLock 객채에 Enter해야 한다.
    bool lockTaken = false;
    try
    {
        _spinlock.Enter(ref lockTaken);
 
        // working
 
        // CPU 사이클 동안 대기
        Thread.SpinWait(500);
 
        // Queue에 넣는건 제외
        // 메모리에 넣고 가비지 컬렉터의 영향을 덜 받기 위해 주석 처리 함.
        //_spinLockQueue.Enqueue(d);
    }
    catch (LockRecursionException ex)
    {
        Console.WriteLine("{0}, {1}"Thread.CurrentThread.ManagedThreadIdex.Message);
    }
    finally
    {
        // 잠김 풀기
        if (lockTaken)
        {
            _spinlock.Exit(false);
        }
    }
}
 
/// <summary>
/// SpinLock 객체로 잠금 병렬 수행
/// </summary>
private static void UseSpinLock()
{
    Stopwatch sw = Stopwatch.StartNew();
 
    // 병렬 실행
    Parallel.Invoke(
            () =>
            {
                for (int i = 0i < Ni++)
                {
                    UpdateWithSpinLock(new LockDataObject() { Name = i.ToString(), Number = i }, i);
                }
            },
            () =>
            {
                for (int i = 0i < Ni++)
                {
                    UpdateWithSpinLock(new LockDataObject() { Name = i.ToString(), Number = i }, i);
                }
            }
        );
 
    sw.Stop();
    Console.WriteLine("elapsed ms with spinlock: {0}"sw.ElapsedMilliseconds);
}
 
/// <summary>
/// Lock 객체로 잠금 상태에서 Queue에 넣기
/// </summary>
/// <param name="d"></param> 
/// <param name="i"></param>
static void UpdateWithLock(LockDataObject dint i)
{
    lock (_lock)
    {
        // working
 
        // CPU 사이클 동안 대기
        Thread.SpinWait(500);
        // Queue에 넣는건 제외
        // 메모리에 넣고 가비지 컬렉터의 영향을 덜 받기 위해 주석 처리 함.
        //_lockQueue.Enqueue(d);
    }
}
 
/// <summary>
/// Lock 객채로 잠금을 사용해서 병렬 수행
/// </summary>
private static void UseLock()
{
    Stopwatch sw = Stopwatch.StartNew();
 
    // 병렬 실행
    Parallel.Invoke(
            () =>
            {
                for (int i = 0i < Ni++)
                {
                    UpdateWithLock(new LockDataObject() { Name = i.ToString(), Number = i }, i);
                }
            },
            () =>
            {
                for (int i = 0i < Ni++)
                {
                    UpdateWithLock(new LockDataObject() { Name = i.ToString(), Number = i }, i);
                }
            }
        );
 
    sw.Stop();
    Console.WriteLine("elapsed ms with lock: {0}"sw.ElapsedMilliseconds);
}

[코드1] SpinLock와 Lock 성능 비교


 소스 코드상의 주석에서도 언급 했듯이 어떤 케이스를 먼저 실행 하느냐에 따라서 결과 값이 상이하게 나오고 있으며 SpinLock를 사용하여 테스트를 하면 프로그램 시작후 가장 처음에 실행되었을때 가장 안 좋은 결과치를 지속적으로 보여주고 있었다. 물론 지금의 성능 비교를 하는 케이스가 SpinLock를 만든 목적에 부합되지 않을 수도 있기 때문에 그런 현상이 발생할 수도 있을 것이다. 두 케이스의 비교를 통해 결론을 내리자면 위 상황에서는 대체적으로 Lock 객체를 사용해서 잠금 발생을 하는것이 성능 우위를 나타내고 있었다. 그렇지만 다른 케이스에서는 어떻게 결과가 나올 지 알수 없으므로 Lock 객체만으로도 성능이 제대로 나오지 않으면 SpinLock를 테스트 해보기를 권한다. 아래 "그림1"에서 결과 화면을 보도록 하자.


[그림1] SpinLock vs Lock 결과 화면

(테스트 환경 : i5 모바일 CPU, 2Core, 하이퍼스레딩, 8GB )



 그리고 Parallel.Invoke에 대해서 부연 설명을 드리자면 Task를 두개 생성하면 실행하는 구문이라고 생각하면 쉽게 이해할 수 있을 것이다. Parallel.Invoke(Task(action), Task(action))와 같은 형식이고 각 타스크가 쓰레드에서 타스크 형식으로 작업이 수행되는 것이다. 두개를 한번에 실행 하기에 잠김 상태가 발생하도록 한 것이다. 그리고 SpinLock()는 해당 CPU 주기만큼 대기 하고 실행하도록 해주는 역할을 한다. Thread.Sleep()는 시간이 비교 대상이라면 SpinLock는 CPU 싸이클이 비교대상인 것이다.


소스 코드 자체에 주석과 직관적인 코딩으로 충분히 파악이 가능할 것으로 예상하므로 별도의 설명을 생략하도록 하겠습니다. 포스트의 내용이 장황한 설명 보다는 주석과 소스코드 자체 만으로도 이해할 수 있도록 하기 위해 노력하였습니다.. 실제 개발에서도 필요한 소스는 단순히 Copy & Paste 만으로도 사용할 수 있습니다. 그리고 주석을 이용해 nDoc이나 별도의 자동 Document 제작 유틸로 API 문서를 만드는 데에도 도움이 되었으면 한다. 
※ DOC에 대한 프로그램 정보 Util link

ing™       


+ Recent posts