Posts C# Thread Synchronization and Locks
Post
Cancel

C# Thread Synchronization and Locks

동기화 영역에 따른 구분


1. 유저 모드 동기화

  • 유저 객체(커널에서 제공하지 않는 객체)를 사용한다.

  • 대표적으로 크리티컬 섹션(Critical Section), 인터락(Interlocked)이 있다.

  • 커널 모드 동기화보다 빠르다.

  • 동일 프로세스 내에서만 동기화 가능하다.


2. 커널 모드 동기화

  • 커널 객체를 사용한다.

  • 대표적으로 뮤텍스(Mutex), 세마포어(Semaphore), 이벤트(Event)가 있다.

  • 다른 프로세스에 존재하는 스레드 간 동기화가 가능하다.

  • 유저 모드에서 커널 모드로 변경해야 하므로, 유저 모드 동기화에 비해 성능 소모가 크다.


동기화 방법론에 따른 구분


1. 양보(Yield)

  • 고전적인 락 기법에 해당한다.
  • 락을 얻지 못할 경우, CPU 자원을 다른 스레드에 양보한다.
  • Lock (Monitor)

2. 바쁜 대기(Busy Waiting)

  • 락을 얻을 때까지 CPU를 점유하면서 무한 대기한다.
  • Spin Lock

3. 이벤트(Event)

  • 락을 획득 가능한 타이밍을 커널로부터 통보받는다.
  • ManualResetEvent, AutoResetEvent


Lock vs. Spin Lock


공통

  • 한 번에 하나의 스레드만 접근 가능한 영역(Critical Section)을 만든다.

  • 하나의 스레드가 이미 영역을 점유한 경우, 진입을 원하는 다른 스레드는 대기한다.


Lock

  • 락을 기다리며 대기하는 스레드는 블록(Block)되며, CPU 점유를 하지 않게 된다.

  • 블록될 때 CPU 자원을 다른 스레드에게 넘기므로 컨텍스트 스위칭으로 인한 오버헤드가 발생한다.

  • 오래 걸리는 작업에 사용한다.


Spin Lock

  • 바쁜 대기(Busy Waiting)가 발생한다.

  • 락을 기다리는 동안에도 CPU 점유를 넘기지 않고, 계속 락 상태를 확인한다.

  • 락이 길게 유지되는 동안에는 대기하는 스레드가 CPU 자원을 계속 소모하므로 낭비가 발생할 수 있다.

  • 컨텍스트 스위칭이 발생하지 않는다.

  • 비교적 짧은 동작을 자주 수행해야 한다면 스핀 락을 사용하는 것이 좋다.


C#의 동기화 방법 정리


[1] Interlocked

  • 특정 변수에 대해 동기화 및 원자적 연산을 수행한다.


[2] Monitor

  • 크리티컬 섹션을 만들고(진입하고), 해제한다.

  • object 타입 매개체가 필요하다.

  • Monitor.Enter(obj) ~ Monitor.Exit(obj)

  • 크리티컬 섹션 내부에서 예외가 발생했을 경우를 위한 try-finally 처리가 필요하다.


[3] lock 구문

  • 내부적으로 Monitor 객체를 이용해 크리티컬 섹션을 만든다.

  • lock(obj) { ~ }


[4] SpinLock

  • 직접 구현하거나 SpinLock 클래스를 사용한다.

  • 락 진입/해제의 간격이 짧고 빈번한 경우에 사용한다.

  • try-finally 처리가 필요하다.


[5] ReaderWriterLock

  • 직접 구현하거나, ReaderWriterLock 또는 ReaderWriterLockSlim 클래스를 사용한다.

  • 읽기 스레드와 쓰기 스레드의 역할이 구분되는 경우에 사용한다.

  • 자주 읽고, 가끔 쓰는 경우에 사용하면 좋다.

  • try-finally 처리가 필요하다.


[6] ManualResetEvent, AutoResetEvent

  • 크리티컬 섹션에 진입하려는 스레드끼리 락을 공유하는 lock 방식처럼 사용할 수 있고,
    해당 락에 관련 없는 다른 스레드가 락의 설정/해제를 관리할 수도 있다.

  • 다수의 스레드의 임계 영역 진입 관리를 해야 할 때 사용한다.

  • 커널 영역 동기화이므로 성능을 고려해야 한다.


[7] Mutex

  • 커널 영역에서의 동기화를 수행하므로 비교적 느리다.

  • 프로세스 간의 데이터 동기화가 필요한 경우 사용한다.


[8] Semaphore

  • 커널 영역에서의 동기화를 수행하므로 비교적 느리다.

  • 프로세스 간의 데이터 동기화가 필요한 경우 사용한다.

  • 다수의 프로세스 또는 스레드가 동시에 크리티컬 섹션에 진입하도록 할 수 있다.


최종 정리 : 스레드 동기화 방법 선택


[1] Interlocked

  • 공유 변수에 대해 원자적으로 읽고, 쓰고, 더하는 경우 간단히 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static int location = 0;
private const int Expected = 1, Desired = 2;

public static void Example()
{
    // 값을 하나 증가시키고, 결과값을 리턴한다.
    int res1 = Interlocked.Increment(ref location);

    // 값을 하나 감소시키고, 결과값을 리턴한다.
    int res2 = Interlocked.Decrement(ref location);

    // 값을 더하고, 결과값을 리턴한다.
    int res3 = Interlocked.Add(ref location, 100);

    // 값을 초기화 하고, 바뀌기 전의 값을 리턴한다.
    int res4 = Interlocked.Exchange(ref location, 100);

    // location의 값이 Expected였을 경우 Desired로 초기화한다.
    // 아닐 경우 초기화하지 않는다.
    // 초기화 이전에 location이 갖고 있던 값을 리턴한다.
    int res5 = Interlocked.CompareExchange(ref location, Desired, Expected);
}


[2] lock 구문

  • 락을 걸고 짧지 않은 동작들을 수행하는 경우에 사용한다.

  • 간편히 lock(){} 구문을 사용하면 된다.

1
2
3
4
5
6
7
8
9
private readonly object _lock = new object();

private void ThreadBodyMethod()
{
    lock (_lock)
    {
        // Do Something
    }
}


[3] SpinLock 클래스

  • 락을 빈번하게 걸고 짧은 동작들을 수행하는 경우에 사용한다.

  • try-finally 처리가 필요하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private SpinLock spinLock = new SpinLock();

private void ThreadBodyMethod()
{
    bool lockTaken = false; // 락을 획득했는지 여부
    try
    {
        spinLock.Enter(ref lockTaken);

        // Do Something Here =========

        // ===========================
    }
    finally
    {
        if (lockTaken)
        {
            spinLock.Exit();
        }
    }
}


[4] ReaderWriterLockSlim 클래스

  • 자주 읽어들이지만 쓰기 수행이 적은 경우에 사용한다.

  • 소수의 쓰기 스레드, 다수의 읽기 스레드로 나뉜 경우에 사용하면 좋다.

  • try-finally 처리가 필요하다.

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
private ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim();

private void WriterThreadBody()
{
    try
    {
        rwLock.EnterWriteLock();

        // Do Something Here =========

        // ===========================
    }
    finally
    {
        rwLock.ExitWriteLock();
    }
}

private void ReaderThreadBody()
{
    try
    {
        rwLock.EnterReadLock();

        // Do Something Here =========

        // ===========================
    }
    finally
    {
        rwLock.ExitReadLock();
    }
}


References


This post is licensed under CC BY 4.0 by the author.