Home [멀티쓰레드] Lock 기초
Post
Cancel

[멀티쓰레드] Lock 기초

Critical Section

코드 상에서 Race Condition(경쟁 상태)이 발생할 수 있는 구역Critical Section(임계 구역)이라고 한다.

Critical Section에서 발생할 수 있는 Race Condition 문제를 해결하기 위해서는 Interlocked 등의 Atomic Operation을 사용해 볼 수 도 있겠지만 간단한 정수 연산만 할 수 있는 등 복잡한 작업을 처리하기에는 한계가 있다.

Race Condition과 Interlocked에 대한 내용

그렇다면 이 Critical Section 문제를 어떻게 해결할 수 있을까.

먼저 Monitor를 사용해볼 수 있다.

Monitor

Monitor는 특정 코드 부분을 배타적으로 Locking하는 기능을 가지고 있다.

C#에서는 아래와 같이 사용해 볼 수 있다.

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
34
35
36
37
38
39
40
static int number = 0;
static object _obj = new object(); // 자물쇠 역할

static void Thread_1()
{
    for (int i = 0; i < 100000; i++)
    {
        Monitor.Enter(_obj); // 자물쇠로 잠군다. (혹시나 잠겨 있으면 기다린다.)

        // Critical Section
        number++;

        Monitor.Exit(_obj); // 잠금을 푼다.
    }
}

static void Thread_2()
{
    for (int i = 0; i < 100000; i++)
    {
        Monitor.Enter(_obj); // 자물쇠로 잠군다. (혹시나 잠겨 있으면 기다린다.)

        // Critical Section
        number--;

        Monitor.Exit(_obj); // 잠금을 푼다.
    }
}

static void Main(String[] args)
{
    Task t1 = new Task(Thread_1);
    Task t2 = new Task(Thread_2);
    t1.Start();
    t2.Start();

    Task.WaitAll(t1, t2);

    Console.WriteLine(number);
}
1
0

Monitor는 쉽게 말해 자물쇠로 잠구고 다른 쓰레드가 못들어오도록 하는 기능이라고 생각하면 된다. 위처럼 다른 쓰레드는 접근을 못하게 하는 것을 Mutual exclusion(상호 배제)이라고 한다.

이처럼 Monitor를 사용하여 Critical Section 문제를 해결해 볼 수도 있지만 다른 문제가 발생할 수 있다.
Monitor.Enter()를 해주면 꼭 마지막엔 Monitor.Exit()를 해주어야 하는데 만약 중간에 예기치 못한 일로 Monitor.Exit()를 해주지 못하면 문제가 발생한다.

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
static int number = 0;
static object _obj = new object(); // 자물쇠 역할

static void Thread_1()
{
    for (int i = 0; i < 100000; i++)
    {
        Monitor.Enter(_obj); // 자물쇠로 잠군다. (혹시나 잠겨 있으면 기다린다.)

        // Critical Section
        number++;

        // 예기치 못한 일로 종료
        return;

        Monitor.Exit(_obj); // 잠금을 푼다.
    }
}

static void Thread_2()
{
    for (int i = 0; i < 100000; i++)
    {
        Monitor.Enter(_obj); // 자물쇠로 잠군다. (혹시나 잠겨 있으면 기다린다.)

        // Critical Section
        number--;

        Monitor.Exit(_obj); // 잠금을 푼다.
    }
}

위처럼 Monitor.Exit()을 해주지 못한채로 작업이 종료되면 다른 쓰레드는 계속 접근을 못한채로 무한히 기다리게 된다. 자물쇠로 잠그고는 풀어놓지 않은채로 가버린 것이다. 이러한 상태을 DeadLock 이라고 한다.

아무튼 이러한 문제는 어떻게 해결해야할까.
일단 try문을 사용해 예외적인 문제가 발생해도 Monitor.Exit()가 수행되도록 할 순 있다.

1
2
3
4
5
6
7
8
9
10
try
{
    Monitor.Enter(_obj);

    // Critical Section
}
finally // try에서 어떤 문제가 발생해도 무조건 실행된다.
{
    Monitor.Exit(_obj);
}

하지만 이렇게 일일이 try finally를 사용해주는 건 매번 번거러운 일이 아닐수가 없다.

그렇기에 좀 더 편리하게 사용할 수 있는 기능이 있다. 바로 lock이다.

lock

아래 코드를 통해 사용법부터 보자면 이전 것보다 훨씬 간편하다는 것을 느낄 수 있을 것이다.

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
static int number = 0;
static object _obj = new object(); // 자물쇠 역할

static void Thread_1()
{
    for (int i = 0; i < 100000; i++)
    {
        lock(_obj)
        {
            // Critical Section
            number++;
        }
    }
}

static void Thread_2()
{
    for (int i = 0; i < 100000; i++)
    {
        lock (_obj)
        {
            // Critical Section
            number--;
        }
    }
}

lock 내부 구현 역시 Monitor로 구현되어 있기 때문에 동작 방식은 비슷하다고 생각하면 된다.

실제로 Monitor를 직접적으로 사용할 일은 별로 없을 것고 보통 위 lock을 많이 사용한다. 다만 동작 방식은 이해해두면 좋기 때문에 같이 알아두면 좋다.

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