경쟁조건, 경합조건이라 불리는 RaceCondition은 멀티 쓰레드 환경에서 공유 자원에 동시에 접근하고 수정하려 할 때 쓰레드의 실행순서나 타이밍에 따라 결과가 달라지는 상황을 얘기한다.
아래 C#코드를 보면 공유 자원(numer)
에 접근하고 수정하는 2개의 쓰레드(Thread1, Thread2)
가 있다.
Thread1은 number를 1증가시키는 연산을, Thread2는 number를 1감소시키는 연산을 각각 10만번씩 한다.
static int number = 0;
static void Thread1()
{
for (int i = 0; i < 100000; i++)
number++;
}
static void Thread2()
{
for (int i = 0; i < 100000; i++)
number--;
}
static void Main(string[] args)
{
for (int i = 0; i < 5; ++i)
{
Task t1 = new Task(Thread1);
Task t2 = new Task(Thread2);
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
Console.WriteLine(number);
}
}
위 코드의 결과를 예상하자면 당연히 0일 것이다. 메인쓰레드에서 t1,t2를 실행시키고 각 loop에 t1,t2를 대기하고 10만번의 증가와 10만번의 감소로 인해 0이 되기 때문이다.
하지만, 각 loop의 결과 값은 0이 아닐 것이다.
이유부터 말하자면 실제 연산되는 순서는 한줄이 아닌 여러줄에 걸쳐서 연산이 이뤄지기 때문이다.
number++
로 예를 들면, 한줄의 코드는 실제 실행될 때 다음과 같은 과정을 거친다.
위 3개의 단계를 Thread1
, Thread2
가 수행한다고 하자 다음과 같은 일이 일어나면 증감을 한번씩 했다해도 number가 0이 아닐 수 있다.
Thread1 Thread2
1. number값을 가져온다. // 0
2. 레지스터 값을 1 증가시킨다. // 1
1. number값을 가져온다. // 0
3. 메모리주소에 레지스터값을 입력한다. // 1
2. 레지스터 값을 1 감소시킨다. // -1
3. 메모리주소에 레지스터값을 입력한다. // -1
위 순서대로 진행된다면 1증가 1감소로 0이 될것이라 예상하지만 number
는 -1이 된다.
위 상황을 해결하기 위해 실제 연산하는 부분에 대해서 다른 쓰레드가 공유 자원에 접근하지 못하도록 할 필요가 있다.
공유자원에 접근하는 코드 부분을 임계구역(Critical Section)이라한다. 이에 대해서는 나중에 글을 작성할 예정이다.
number의 증감연산을 한번의 연산으로 나타내기 위해 C#에는 InterLocked
가 존재한다. 하지만, InterLocked는 정수값에 대한 연산의 동시성을 제어해주기 때문에 더 복잡한 상황에서는 다른 방법을 사용해야 한다.
문제상황을 해결하기 위해 Thread1
, Thread2
의 내용을 다음과 같이 바꿔준다. Interlocked.Increment
와 Interlocked.Decrement
를 사용함으로 증감연산의 결과를 보장받을 수 있고 실행결과는 0이 나오게 된다.
static void Thread1()
{
for (int i = 0; i < 100000; i++)
Interlocked.Increment(ref number);
}
static void Thread2()
{
for (int i = 0; i < 100000; i++)
Interlocked.Decrement(ref number);
}