이전에 스레드 사용에 대한 방법을 알아보았으니, 이번에는 동기화에 대해 알아보겠습니다. 동기화에 알아보기에 앞서 왜 동기화를 사용하는지 먼저 설명하겠습니다.
프로세스를 구성하는 각 스레드는 여러 가지 자원을 공유하는 경우가 많습니다. 만약 다른 스레드가 어떤 자원을 잡고 사용하고 있는 중인데 갑자기 끼어들어 사용할 수 있는게 스레드입니다.
이 스레드들을 그냥 내버려둔다면 우리가 만든 소프트웨어들은 모두 엉망이 되고 말 것입니다. 그래서 프로그래머들은 스레드들이 질서있게 자원을 사용할 수 있도록 구성해야할 책임이 있습니다. 이 처럼 스레드들이 순서를 갖춰 자원을 사용하게 하는 것을 일컬어 '동기화(Synchronization)'이라고 합니다. 이것을 제대로 하는 것이야 말로 멀티 스레드 프로그래밍을 완벽하게 하는 길이라고 할 수 있습니다.
스레드 동기화에서 가장 중요한것은 '자원을 한번에 하나의 스레드가 사용하도록 보장'하는 것입니다. 그래서 .NET프레임워크는 대표적으로 두개의 동기화 방법을 제공하는데 lock키워드와 Monitor클래스입니다.
두 방법의 차이점을 먼저 말씀드리자면 lock키워드는 사용이 쉽고, Monitor클래스는 더 섬세한 동기화 제어 기능을 제공합니다.
먼저 lock 동기화에 대해 알아보겠습니다.
한번쯤 '크리티컬 섹션(Critical Section)'에 대해 들어보았을것입니다. 크리티컬 섹션은 한번에 한 스레드만 사용할 수 있는 코드 영역을 말하는데, C#에서는 lock키워드로 감싸주기만 해도 평범한 코드를 크리티컬 섹션으로 바꿀 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
|
public int count = 0;
private readonly object thislock = new object();
// 스레드가 실행할 메소드
public void ThreadFunction()
{
lock (thislock)
{
count = count + 1;
}
}
| cs |
Lock키워드와 {}괄호로 둘러싼 이 부분은 크리티컬섹션이 됩니다. 한 스레드가 이 코드를 실행하다가 lock블록이 끝나는 괄호를 만나기 전까지는 다른 스레드는 절대 이 코드를 실행할 수 없습니다.
Lock 동기화는
위 코드에서 보았듯이 사용하기가 매우 간편합니다. 하지만 간편하다고 막 난무하면 안되겠지요. 왜냐하면, 자원을 획득하지 못한 다른 스레드들은 자기들도 크리티컬 섹션을 만들어야하니 락을 얻기위해 대기하는 상황이 생깁니다. 이런 경우 소프트웨어의 성능이 크게 떨어집니다. 따라서 스레드의 동기화를 설계할 때는 크리티컬 섹션을 반드시 필요한 곳에만 사용하도록 하는 것이 중요합니다.
한편 lock키워드의 매개 변수로 사용하는 객체는 참조형이면 어느 것이든 쓸 수 있지만, public 키워드 등을 통해 외부 코드에서도 접근할 수 있는 다음 세 가지는 절대 사용하지 않기를 권합니다.
1. This : 클래스의 인스턴스는 클래스 내부뿐만 아니라 외부에서도 자주 사용됩니다. 그래서 lock(this)는 사용해서는 안됩니다.
2. Type : typeof연산자나 object클래스로부터 물려받은 GetType()메소드는 type형식의 인스턴스를 반환합니다. 즉 코드의 어느 곳에서나 특정 형식에 대한 Type객체를 얻을 수 있습니다.
Lock(typeof(SomeClass))나 lock(obj.GetType())은 피해야 합니다.
3. String : 절대 string객체로 lock을 하시면 안됩니다. "abc"는 어떤 코드에서든 얻어낼 수 있는
string객체입니다. Lock("abc")와 같은 코드는 사용해서는 안됩니다.
두번째로 Monitor클래스 동기화입니다.
Monitor클래스는 스레드 동기화에 사용하는 몇 가지 정적 메소드를 제공합니다. 그 중 Monitor.Enter()와 Monitor.Exit()입니다.
이 두 메소드는 앞에서 설명했던 lock키워드와 완전히 똑같은 기능을 합니다.
Monitor.Enter()메소드는 크리티컬 섹션을 만들며 Monitor.Exit() 메소드는 크리티컬 섹션을 제거합니다. Lock에서는 {} 블록에 해당하지요.
1
2
3
4
5
6
7
8
9
10
11
12
|
public void ThreadFunction()
{
Monitor.Enter(thislock);
try
{
count = count + 1;
}
finally
{
Monitor.Exit(thislock);
}
}
| cs |
사실 lock키워드는 Monitor클래스의 Enter()와 Exit() 메소드를 바탕으로 구현되어 있습니다. 그러니 Monitor.Enter()와 Monitor.Exit()로 동기화할 것 같으면 차라리 lock키워드를 사용하는 편이 낫습니다. 코드도 읽기가 좋고 Monitor.Exit() 메소드를 잘못 사용하게 될 경우 크리티컬 섹션이 해지되지 않는상황이 발생할 수 도 있어 프로그램에 버그가 생길 가능성도 있습니다.