Interlocked 클래스는 int 형 값을 증가시키거나 감소시키는데 사용한다. 멀티 스레드 환경에서 하나의 int 형 전역 변수를 공유한다고 생각해보자. 이런 경우에 A 쓰레드와 B 쓰레드가 값을 동시에 읽어와서 B 쓰레드가 수정한 값을 저장하고, A 쓰레드가 다시 수정한 값을 저장하게 되면 B 쓰레드의 변경사항을 잃어버리게 된다.
지금까지 이러한 자원의 동기화를 위해서 모니터나 뮤텍스를 사용하는 방법을 설명했지만 간단한 int 형의 값을 여러 쓰레드가 공유하는 것이 일반적이기 때문에 이러한 작업을 캡슐화한 클래스를 제공한다.
Interlocked 클래스는 System.Threading 클래스에 있으며 주요 멤버는 다음과 같다.
메소드 이름
|
설 명
|
CompareExchange | 두 대상을 비교하여 값이 같으면 지정된 값을 설정하고, 그렇지 않으면 연산을 수행하지 않는다. |
Decrement | 지정된 변수의 값을 감소시키고 저장한다. |
Exchange | 변수를 지정된 값으로 설정한다. |
Increment | 지정된 변수의 값을 증가시키고 저장한다. |
public static int Increment(ref int);
public static long Increment(ref long);
public static int Decrement(ref int);
public static long Decrement(ref long);
using System;
using System.Threading;
public class AppMain
{
private int m_member;
public static void Main()
{
AppMain ap = new AppMain();
ap.DoTest();
}
private void DoTest()
{
Thread thread1 = new Thread(new ThreadStart(Incrementer));
Thread thread2 = new Thread(new ThreadStart(Decrementer));
thread1.Start();
thread2.Start();
}
private void Incrementer()
{
Random rdm = new Random( unchecked((int)DateTime.Now.Ticks) );
for ( int loopctr = 0; loopctr < 10; loopctr++)
{
Interlocked.Increment(ref m_member);
Console.WriteLine("Incrementer : {0}", m_member.ToString());
Thread.Sleep(rdm.Next(1, 200));
}
}
private void Decrementer()
{
Random rdm = new Random( ~unchecked((int)DateTime.Now.Ticks) );
for ( int loopctr = 0; loopctr < 10; loopctr++)
{
Interlocked.Decrement(ref m_member);
Console.WriteLine("Decrementer : {0}", m_member.ToString());
Thread.Sleep(rdm.Next(1, 300));
}
}
}
Incrementer 메소드를 위임받은 thread1은 1을 10번 증가시키고, Decrementer 메소드를 위임받은 thread2는 1을 10번 감소시킨다. 결과적으로 0이 된다.
위 코드에서 Incrementer와 Decrementer 대신에 동기화를 수행하지 않는 ++과 ?로 바꾸고, Thread.Sleep()을 제거한다면 위와 같이 되지 않는다 이는 디버거 수준에서 확인하거나 2개 이상의 CPU가 장착된 시스템에서 관찰할 수 있다.
private int m_member; // 쓰레드간에 공유할 변수를 선언한 것이다.
private void Incrementer()
{
Random rdm = new Random( unchecked((int)DateTime.Now.Ticks) );
for ( int loopctr = 0; loopctr < 10; loopctr++)
{
Interlocked.Increment(ref m_member);
Console.WriteLine("Incrementer : {0}", m_member.ToString());
Thread.Sleep(rdm.Next(1, 200));
}
}
Incrementer 함수는 Interlocked.Increment를 사용하여 값을 증가시키는 메소드다. Interlocked.Increment에 보면 ref 키워드를 같이 사용하는 것에 주의한다. Decrementer 함수는 Incrementer 함수와 동일하며 Interlocked.Increment 대신에 Interlocked.Decrement를 사용한다.
Random rdm = new Random( unchecked((int)DateTime.Now.Ticks) );
Random rdm = new Random( ~unchecked((int)DateTime.Now.Ticks) );
위 둘은 각각의 함수에서 사용한 것인데, 동시에 두 개의 쓰레드가 다른 값을 얻기 위해 위와 같은 코드를 사용한 것이다. 여러분이 난수를 생성하기 위해 시스템에서 가져오는 시간값은 1/3 ms초마다 갱신되기 때문에 두 개의 쓰레드가 값을 가져오는 간격이 1/3 ms 이하인 경우에 같은 값을 가져오게 된다.(시스템마다 차이가 있다) ~는 NOT 연산을 수행하며, 비트를 변경한다. unchecked는 가져온 값을 int 형으로 가져올 때 발생하는 오버플로우 검사를 수행하지 않도록 한 것이다. unchecked를 사용하지 않으면 OverflowException 예외가 발생한다.
Interlocked의 함정
Interlocked를 사용하여 정수형 값을 수정하기로 했다면 직접 멤버 변수에 값을 설정하려 해서는 안된다. 다음과 같은 코드를 생각해보자.
private int m_member;
m_member++; // 잘못된 방법
Interlocked.Increment(ref m_member);
여러 쓰레드가 항상 유효한 값을 갖도록 하려면 변수에 직접 액세스하지 않고 Interlocked 클래스를 사용하여 액세스해야한다. Interlocked 클래스를 사용할 때 알아야 할 점은 하나의 쓰레드가 변수 값을 읽는 동안에 다른 쓰레드가 Interlocked.Increment를 호출하는 경우에도 항상 변수 값은 유효하다는 것이다. 즉, 쓰레드는 Interlocked.Increment에 의해 변경되기 전의 변수 값을 얻거나 아니면 Interlocked.Increment에 의해 변경된 변수 값을 얻게 된다. 즉, Interlocked 클래스는 쓰레드가 정확히 어느 시점의 값을 얻을지 알 수 없지만, 부분적으로 증가된 값이 아니라 항상 유효한 값을 얻도록 보장한다. 닷넷의 Interlocked 클래스의 구현에 대해서 알고 싶다면 C#과 CLI에 대한 공유 소스 코드를 다운 받은 다음에 .\sscli\clr\src\vm\syncblk.cpp 소스 코드를 참고한다.
출처 : https://m.hanbit.co.kr/network/category/category_view.html?cms_code=CMS2117503016
'CS > 네트워크' 카테고리의 다른 글
Overlapped (비동기) I/O, epoll, iocp 정의 및 코드 (0) | 2022.11.04 |
---|---|
TCP와 UDP의 특징과 차이 (0) | 2022.11.04 |
C++ 원자적 연산 (atomic) (0) | 2022.07.26 |
Thread 사용법 및 생성 (0) | 2022.07.24 |
데드락 (Deadlock) 의미 & 조건 (0) | 2022.07.24 |