아래와 같이 코드를 실행해보자.
namespace SeverCore
{
class Program
{
static int number = 0;
static void Thread_1()
{
for (int i = 0; i < 10000; i++)
number++;
}
static void Thread_2()
{
for (int i = 0; i < 10000; 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);
}
}
}
예상처럼 0이 결과로 뜨는 것을 알 수 있다.

숫자를 100000으로 늘린다면?

왜 이런 결과가 나올까?
number++; 부분에 집중해보자.
위 한줄의 코드는 아래 세줄로 해석된다.
int temp = number;
temp +=1;
number = temp;
코드 상으로 볼 때는 한가지 단계로 생각했지만 사실상 세가지의 단계로 이루어진 것이다.
즉 원자성(atomic)의 개념이 중요해진다.
어떤 동작이 한 번에 일어나야한다는 것이다.
이를 해결하기 위해 Interlocked 키워드를 사용하자.
단, Interlocked를 사용할 경우 성능에 영향을 준다는 단점이 있다.
namespace SeverCore
{
class Program
{
static int number = 0;
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);
}
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);
}
}
}
Interlocked 키워드 같은 경우 메모리 배리어를 간접적으로 사용하고 있다.
즉 가시성 문제가 해결된다.
Interlocked.Increment(ref number);
number의 값을 참조해서 사용해야하기 때문에 ref로 인자를 받는다.
해당 연산이 지난 후의 값을 이용하고 싶을 경우
아래와 같이 int 인자로 받아 사용한다.
int afterValue = Interlocked.Increment(ref number);
why?) 멀티 쓰레드 환경에서는 number를 다른 누군가가 사용하고 있을 수 있기 때문이다.