Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
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
Archives
Today
Total
05-16 20:20
관리 메뉴

nomad-programmer

[Programming/C#] 스레드 동기화 (Thread Synchronization) 본문

Programming/C#

[Programming/C#] 스레드 동기화 (Thread Synchronization)

scii 2020. 9. 29. 02:20

애플리케이션을 구성하는 각 스레드는 여러 가지 자원을 공유하는 경우가 많다. 파일 핸들이나 네트워크 커넥션, 메모리에 선언한 변수 등이 그 예다. 그런데 공유되는 자원을 여러 스레드들이 한꺼번에 변경시키려하면 문제가 발생한다. 그래서 스레드들이 정연하게 자원을 사용할 수 있도록 질서를 잡아야 할 필요가 있다.
스레드들이 순서를 갖춰 자원을 사용하게 하는 것을 일컬어 "동기화(Synchronization)" 라고 한다. 이것을 제대로 하는 것이야말로 멀티 스레드 프로그래밍을 완벽하게 하는 길이라고 할 수 있다.

스레드 동기화에서 가장 중요한 것은 "자원을 한번에 하나의 스레드가 사용하도록 보장" 하는 것이다.
.NET 프레임워크가 제공하는 대표적인 도구로는 lock 키워드와 Monitor 클래스가 있다. 이 둘은 하는 일이 거의 유사하지만 lock 키워드가 사용가기 더 쉽다. 허나 Monitor 클래스가 더 섬세한 동기화 제어 기능을 제공하고 있다.

lock 키워드 동기화

크리티컬 섹션(Critical Section)은 한번에 한 스레드만 사용할 수 있는 코드 영역을 말한다. C#에서는 lock 키워드로 감싸주기만 해도 평범한 코드를 크리티컬 섹션으로 바꿀 수 있다. 

class Counter
{
    public int count = 0;
    public void Increase()
    {
        count = count + 1;
    }
}

// ...

Counter counter = new Counter();
Thread t1 = new Thread(new ThreadStart(counter.Increase));
Thread t2 = new Thread(new ThreadStart(counter.Increase));
Thread t3 = new Thread(new ThreadStart(counter.Increase));

t1.Start();
t2.Start();
t3.Start();

t1.Join();
t2.Join();
t3.Join();

// count 값은????
Console.WriteLine(counter.count);

위의 코드에서 counter.count 값은 어떤 값일지 예상할 수 없다. 3일 수도 있고 아닐 수도 있다. counter.count는 최소 1부터 최대 3의 결과를 가질 수 있다. 
원인은 count = count + 1 코드에 있다. t1 스레드가 Increase() 메소드를 한참 실행하다 미처 연산을 마치기 전에 t2 스레드가 같은 코드를 실행하고, t2가 아직 연산을 마치지 않았는데 t3도 같은 코드를 실행하면 counter.count 는 0인 채로 연산을 당하다가 세 개의 스레드가 작업을 마쳤는데도 값은 1에 불과한 결과를 맞게 된다.
더 심각한 문제는 위 코드를 실행할 때마다 결과가 1, 2, 3 중 어떤 것이 나올지 모른다는 점이다.

이 문제를 해결하기 위해서는 count = count + 1 코드를 한 스레드가 실행하고 있을 때 다른 스레드는 실행하지 못하도록 하는 장치가 필요하다. 그 장치가 바로 "크리티컬 섹션 (임계 영역)" 이다. C# 에서는 lock 키워드를 이용해서 간단하게 크리티컬 섹션을 만들 수 있다.

class Counter
{
    public int count = 0;
    public void Increase()
    {
        // 크리티컬 섹션
    	lock (thislock)
        {
            count = count + 1;
        }
    }
}

// ...

Counter counter = new Counter();
Thread t1 = new Thread(new ThreadStart(counter.Increase));
Thread t2 = new Thread(new ThreadStart(counter.Increase));
Thread t3 = new Thread(new ThreadStart(counter.Increase));

t1.Start();
t2.Start();
t3.Start();

t1.Join();
t2.Join();
t3.Join();

// count 값은????
Console.WriteLine(counter.count);

lock 키워드와 중괄호가 둘러싼 부분이 크리티컬 섹션이 된다. 한 스레드가 이 코드를 실행하다가 lock 블록이 끝나는 괄호를 만나기 전까지는 다른 스레드는 절대 이 코드를 실행할 수 없다.

스레드의 동기화를 설계할 때는 크리티컬 섹션을 반드시 필요한 곳에만 사용하도록 하는 것이 중요하다.
그렇지 않으면 큰 성능 하락이 발생한다.

lock 키워드의 매개 변수로 사용하는 객체는 참조형이면 어느 것이든 쓸 수 있지만, public 키워드 등을 통해 외부 코드에서도 접근할 수 있는 다음 세 가지는 절대 사용하지 않아야 한다.
이들을 사용하는 것은 문법적으로 아무 문제가 없기 때문에 컴파일 검사를 통과하지만 다른 자원에 대해 동기화를 해야 하는 스레드도 예기치 않게 "lock을 얻기 위해 대기하는 상황"을 만들기 때문이다.

  • this : 클래스의 인스턴스는 클래스 내부뿐만 아니라 외부에서도 자주 사용된다. 자주 정도가 아니고 거의 항상 그렇다. lock(this)는 나쁜 버릇이다.
  • Type 형식 : typeof 연산자나 object 클래스로부터 물려받은 GetType() 메소드는 Type 형식의 인스턴스를 반환한다. 즉, 코드의 어느 곳에서나 특정 형식에 대한 Type 객체를 얻을 수 있다. lock(typeof(SomeClass)) 혹은 lock(obj.GetType()) 은 피해야 한다.
  • string 형식 : 절대 string 객체로 lock을 사용하면 안된다. "abc"는 어떤 코드에서든 얻어낼 수 있는 string 객체이다. lock("abc") 와 같은 코드는 절대 사용하면 안된다.

이 정도 주의 사항을 염두에 둔다면, lock을 이용해서 스레드가 공유하는 자원을 동기화하는 데 별 문제가 없을 것이다.

lock을 이용한 스레드 공유 자원 동기화 예제

using System;
using System.Threading;

namespace CSharpExample
{
    class Counter
    {
        const int LOOP_COUNT = 1000;
        readonly object thisLock;

        public int Count { get; set; }

        public Counter()
        {
            thisLock = new object();
            Count = 0;
        }

        public void Increase()
        {
            int loopCount = LOOP_COUNT;
            while (loopCount-- > 0)
            {
                lock (thisLock)
                {
                    Count++;
                }
                Thread.Sleep(1);
            }
        }

        public void Decrease()
        {
            int loopCount = LOOP_COUNT;
            while (loopCount-- > 0)
            {
                lock (thisLock)
                {
                    Count--;
                }
                Thread.Sleep(1);
            }
        }

    }
    internal class MainApp
    {
        static int Main(string[] args)
        {
            Counter counter = new Counter();

            Thread incThread = new Thread(new ThreadStart(counter.Increase));
            Thread decThread = new Thread(new ThreadStart(counter.Decrease));

            incThread.Start();
            decThread.Start();

            incThread.Join();
            decThread.Join();

            Console.WriteLine(counter.Count);

            return 0;
        }
    }
}


/* 결과

0

*/
Comments