lock 과 Monitor 임계영역
Monitor
이를 위해 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을 고려해야 하는 골치아픈 단점이 있어 거의 대부분 쓰이지 않는다.
처럼 Enter, Exit으로 샌드위치처럼 코드를 감쌌지만
lock(_obj){
//소스코드
}
Lock에선 이와 같이 소스코드를 넣어주면, lock 구절이 끝난 이후엔 자동으로 Monitor.Exit을 하게 되는 효과를 가지게 된다.
lock은 thread-unsafe(안전하지 않은 스레드)코드를 안전하게 사용하고자 할때 사용한다. 즉, 동기화 처리를 할때 사용하는 구문이다.
그렇다면, thread-unsafe한 코드는 어떤 코드인가 다음 예제를 보자.
static public class Division
{
static int num1 = 100, num2 = 5;
static public void Divide()
{
try
{
if (num2 != 0) Console.WriteLine(num1 / num2);
num2 = 0;
}
catch (Exception e)
{
Debug.WriteLine(string.Format("Err : {0}", e));
}
}
}
Divide 함수가 스레드 하나의 함수에 의해 호출되면 안전하게 동작하겠지만, 두개 이상의 스레드에서 동시에 실행되면 thread-unsafe한 상태가 된다.
첫번째 실행된 스레드가 num2 = 0으로 할당하는 시점에 두번째 실행된 스레드가 num1/num2를 수행하게되면 DivideByZeroException("0으로 나누려 했습니다.")을 발생시키면서 정상 동작을 못하는 상황이 발생될 수 있기때문이다.
thread-safe(스레드에 안전한 코드)로 만들려면 아래 클래스와 같이 수정해주면된다.
static public class Division
{
static int num1 = 100, num2 = 5;
static readonly object divisionlocker = new object();
static public void Divide()
{
try
{
lock (divisionlocker)
{
if (num2 != 0) Console.WriteLine(num1 / num2);
num2 = 0;
}
}
catch (Exception e)
{
Debug.WriteLine(string.Format("Err : {0}",e));
}
}
}
lock 구문에 의해 첫번째로 실행된 스레드의 코드 처리 위치가 lock 구문안에 있다면, 두번째로 실행된 스레드는 첫번째 실행된 스레드가 lock구문 밖으로 나올때까지 기다렸다가 실행하여 thread-safe하게 동작한다.
ref : https://sixthman23.tistory.com/entry/lock-%EA%B7%B8%EB%A6%AC%EA%B3%A0-MonitorEnterMonitorExit