프로그래밍(Programming)/C#

Interlocked.Increment 과 Race Condition

3DMP 2022. 11. 26. 03:45
class Program
    {

        static int number = 0; 

        static void Thread_1()
        {
            for(int i=0; i<100000; i++)
            {
                number++; 
            }
        }

        static void Thread_2()
        {
            for(int i=0; i<100000; i++)
            {
                number--; 
            }
        }

        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);
        }
   }

number에 1을 더하는 연산을 10만번, number에 1을 빼는 연산을 10만번 수행한다.

우리는 당연히 number에 0이 할당되었을 것이라 생각 할수 있지만 그렇지 않다

 

하지만 결과는 전혀 엉뚱한 값이 나온다.

이런 현상이 발생하는 이유는 Race Condition(경합 조건) 때문이다.

 

 

 

코드를 자세히 살펴 보자.

number++;

number--; 

두 코드는 어셈블리 언어의 관점에서 아래와 같이 해석할 수 있다.

다른 스레드
int temp = number; 

temp += 1; 

number = temp; 



다른 스레드
int temp = number; 

temp -= 1;

number = temp;

 


전역변수의 값을 다른 스레드에서 각각 서로 변경하려다보니 이전 값으로 덮어 쓰게 되어 문제가 발생함

 

이것은 가시성 문제가 아니다

 

 

이러한 문제를 해결하려면 원자성(Atomicity)을 보장해주면 된다.

원자성이란? 더 이상 쪼개질 수 없는 성질로써, 어떠한 작업이 실행될때 언제나 완전하게 진행되어 종료되거나, 그럴 수 없는 경우 실행을 하지 않는 경우를 말한다

즉, number++와 number-- 연산이 원자성을 보장하여 한꺼번에 실행되거나 아예 실행하지 않도록 해주면 된다.

 

 

static void Thread_1()
{
	for(int i=0; i<100000; i++)
    	{
    		Interlocked.Increment(ref number);
    	}
}

static void Thread_2()
{
	for(int i=0; i<100000; i++)
	{
    		Interlocked.Decrement(ref number); 
	}

Interlocked 계열은 All or Nothing. 즉, 실행을 하거나 안되거나 이다.

Interlocked 하나가 실행되면 다른 Interlocked은 대기하고 있는다.

이렇게 함으로써 동시다발적으로 경합을 하면 최종승자만이 작업을 할 수 있고 결과를 보장받을 수 있다.

하지만 작업 속도는 당연히 원래 연산보다 느려질 수 밖에 없다. 한 작업이 끝날 때 까지 다른 작업이 대기해야 하기 때문

 

 

번외로, 만약에 우리가 number란 값을 추출하고 싶다면 어떻게 해야할까?

for(int i=0; i<100000; i++)
{
	prev = number; 
	Interlocked.Increment(ref number);
	next = number; 
}
int afterValue = Interlocked.Increment(ref number);

위 코드와 같이 Interlocked 함수의 반환 값을 통해 number의 값을 추출해야 한다.

 

이렇게 바로 받아오는건 스레드 세이프한데

 

아래 처럼 처리하면 next = number; 에서 스레드 세이프 하지 않게 된다

이 순간다른 스레드가 값을 바꿀 수있다

for(int i=0; i<100000; i++)
{
	prev = number; 
	Interlocked.Increment(ref number);
	next = number; 
}
반응형