Interlocked
지난 시간에 Interlocked를 배웠다. 그러나 Interlocked는 정수 증가일 때만 사용할 수 있는 단점이 있다. 여러줄의 코드를 작성하되, 다른 쓰레드가 접근하지 않도록 하려면 무엇을 해야할까?
이를 위해 Monitor 라는 기능이 존재한다.
사용하기 전 변수를 선언해주자.
static object _obj=new object();
Monitor는 다음과 같이 사용하면 된다.
Monitor.Enter(_obj);
//소스코드
Monitor.Exit(_obj);
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ServerCore
{
class Lock_Basic
{
//1. 전역변수 메모리 값을 레지스터에 갖고오고, 레지스터 값을 증가시키고, 그 값을 메모리 값에 넣는다
static object _obj = new object();
static int number = 0;
static void Thread_1()
{
for (int i = 0; i < 10000; i++)
{
Monitor.Enter(_obj);
number++;
Monitor.Exit(_obj);
}
}
static void Thread_2()
{
for (int i = 0; i < 10000; i++)
{
Monitor.Enter(_obj);
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);
}
}
}
이와 같이 실행한다면 증감연산자는 원자적으로 동작하지 않음에도 불구하고 잘 작동하는 점을 확인할 수 있을 것이다.
아시다시피 동시 다발적으로 쓰레드가 공유 변수에 접근시 문제가 발생했고, 이를 우리는Critical Section(임계영역)이라고 첫 시간때 이야기를 하였다.
그러나 Monitor.Enter, Monitor.Exit을 통해 이를 방지하여 이 사이의 코드는 사실상 싱글 쓰레드와 같은 상태가 되어버렸다. 이러한 조치를 Mutual Exclusion(상호배제)라고도 설명하였다.
Enter, Exit은 쉽게 이야기하면 1인용 화장실과 같다.
Enter(화장실 들어가서 문잠금)를 통해 한 사람이 화장실을 이용하고 있으면 Exit(화장실을 다 쓰고 문 열기)하기 전까지 바깥의 사람들은 화장실을 사용할 수 없다.
적어도 그 순간만큼은 소스코드에 다른 쓰레드가 끼어들 껀덕지가 없다는 뜻이다.
하지만, Monitor의 경우에도 단점이 있는데, 만약 Exit를 선언하지 않게 된다면 어떻게 될까? 화장실은 계속 이용하고 있고, 영영 화장실을 나가지 않게 되면 바깥의 사람들은 주구장창 기다려야 할 것이다.
이러한 상황을 데드락이라고 하는데, 이는 다음 시간에 자세히 살펴볼 것이다.
따라서 Monitor를 쓰는 경우 모든 경우(예외 처리)까지 Exit을 고려해야 하는 골치아픈 단점이 있어 거의 대부분 쓰이지 않는다.
Monitor의 단점을 개선한 Lock을 대부분 사용하게 된다.
Monitor의 경우
Monitor.Enter(_obj);
//소스코드
Monitor.Exit(_obj);
처럼 Enter, Exit으로 샌드위치처럼 코드를 감쌌지만
lock(_obj){
//소스코드
}
Lock에선 이와 같이 소스코드를 넣어주면, lock 구절이 끝난 이후엔 자동으로 Monitor.Exit을 하게 되는 효과를 가지게 된다. 처음 나온 코드에 Monitor 대신 Lock을 이용하여 구현해보자.