지난 시간에는 Memory Barrier를 활용하여 최적화 시에 자기 맘대로 코드의 순서를 변경하는 걸 막아주거나, 가장 최신 값을 값을 불러오거나 대입하는 가시성을 살려주는 역할을 한다고 배웠다.
오늘은 비슷한 거 같지만 다른 주제로 Interlocked 라는 개념에 대해 배울 것이다.
이렇게 반복하면서 더 증가시키는 쓰레드와, 감소시키는 쓰레드가 있고 둘을 모두 실행시킬 경우 최종 num의 값을 얼마일까?
단순하게 1000번 더하고, 1000번 빼준거니까 0이라는 숫자를 예상할 수 있다.
하지만 실행할 때마다 조금 씩 달라지지만 이렇게 예상과는 전혀 다른 값이 출력 된다.
우선 '++', '--" 라는 친구들이 어떤 방식으로 작동하는 지 알아야 한다. 우리는 num++
과 같이 증감연산자를 통해 간편하게 숫자를 올리지만 사실 컴퓨터에서는 아래와 같은 단계로 구분이 되어 실행된다.
int tem = number;
tem += 1;
number = tem;
이렇게 처리 될 경우 공유 자원의 다른 스레드들이 서로 읽고 쓰는 과정이 생기면서 실행결과가 달라지는 상황이 발생한다. 이를 Race Condition이라고 표현한다.
Race Condition이 일어나는 경우
의도 대로라면 number++을 했을 경우 1이 나와야하나, 아래 같은 상황이 펼처지면 0이 나오는 경우가 생길 수도 있는 것이다.int tem = number; /* 여기서 갑자기 다른 스레드에서 계산한 number 가 들어간다면? ex. number 값은 -1이 들어왔다 */ tem += 1; number = tem; // 출력시 number의 값은 0이다.
이때 필요한 개념이 Atomic(원자성)이다
Atomic이란?
더이상 쪼개질 수 없으며 중단되지 않는 연산의 성질을 의미한다. 이렇게 세개로 구분되어진 작업이 하나로 엮여져있어 이 모든 연산이 끝난 후에 다음 것이 이루어지는 것을 의미한다.int tem = number; // 실제는 이런 임시 변수가 아닌 레지스터에 저장됨 tem += 1; number = tem;
atomic 개념이 필요한 경우를 게임에서 예시로 들면
그렇기에 한 번에 실행되어야 하는 상황들에서는 원자성을 보존해주어야한다.
Interlocked는 C#에서 원자성이 읽기, 쓰기, 증가, 감소 연산에 대해 원자성을 보장해준다.
사용방법은 매우 간단하다. Interlocked 를 사용해주면 원자성을 보장 받은 상태로 구동되기에 0이라는 값을 정확히 출력 받을 수 있다.
Interlocked.Increment(ref num)
증감
Interlocked.Decrement(ref num)
감소
다만 앞서 언급한대로 당연히 성능저하가 있으니 여기저기 남발하지 않는 것이 좋을 것 같다.
만약 증감 된 값을 가져오고자 할 때 이렇게 받아오면 과연 정확하게 1이 증가 된 num의 값을 가져올까?
싱글 스레드 환경이라면 모르지만 멀티 스레드 환경에서는 '100% 된다'고 보장할 수는 없다
그렇기에 결과 값을 정확하게 받아와야 한다면 이런식으로 반환 값을 받아 사용해주어야한다.
int r1 = Interlocked.Increment(ref num);
강의 마지막에서 ref 키워드를 왜 사용했는지 생각해보라고 하셨다.
ref 키워드란
매개 변수에서 참조한 값이 함수 내에서 변동이 있을 경우 직접 반영되도록 만드는 키워드이다.
Interlocked.Increment(ref num)에서 매개변수로 ref 키워드를 통해 받게 되는건 당연하게 참조하고 있는 값을 직접 바꿔야지 증가시킨 값이 될 수있기 때문이다.
만약 그냥 값을 넘겨줄 경우 return 값을 받아 다시 넣어주거나해야 할텐데 이러면 멀티 스레드에서 또 문제 발생 가능하기에 ref 키워드를 통해 바로 변경 시켜주는 것 같다.