본문 바로가기
Programming/C#

C# 멀티스레딩에서 경합 조건과 Interlocked 클래스의 사용

by Dev_카페인 2024. 7. 1.
반응형

C# 멀티스레딩에서 경합 조건과 Interlocked 클래스의 사용

예제 코드 소개

아래의 예제 코드는 두 개의 스레드가 number 변수를 각각 증가 및 감소시키는 방식으로 동작합니다. 하지만, 실행 결과는 예상과 다르게 0이 아닌 다른 값이 출력될 수 있습니다.

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

경합 조건이란?

경합 조건(race condition)이란 여러 스레드가 동시에 공유 자원에 접근할 때 발생할 수 있는 문제입니다. 각 스레드가 자원을 읽고 쓸 때의 순서에 따라 프로그램의 실행 결과가 달라질 수 있습니다.

예제 코드의 문제

위 코드에서 두 스레드 Thread_1과 Thread_2는 각각 number 변수를 10,000번씩 증가 및 감소시킵니다. 이론적으로는 최종 결과가 0이 되어야 하지만, 실행할 때마다 다른 값이 출력될 수 있습니다.

어셈블리 수준에서의 동작

어셈블리 코드에서 number++와 number-- 연산은 세 단계로 나뉩니다:

  1. 값을 읽는다.
  2. 값을 수정한다.
  3. 값을 쓴다.

예를 들어, number++ 연산은 다음과 같이 어셈블리 코드로 번역될 수 있습니다:

mov eax, [number] ; number 값을 레지스터로 이동
add eax, 1        ; 레지스터 값 증가
mov [number], eax ; 증가된 값을 number에 저장

두 스레드가 동시에 number 변수에 접근할 때, 다음과 같은 상황이 발생할 수 있습니다:

  1. 스레드 1이 number 값을 읽는다.
  2. 스레드 2가 number 값을 읽는다.
  3. 스레드 1이 값을 증가시킨다.
  4. 스레드 2가 값을 감소시킨다.
  5. 스레드 1이 값을 쓴다.
  6. 스레드 2가 값을 쓴다.

이 경우, 스레드 1의 연산이 스레드 2의 연산에 의해 덮어쓰여 최종 결과가 예상과 다르게 됩니다.

Interlocked 클래스 사용

Interlocked 클래스는 원자적(atomic) 연산을 제공하여 경합 조건을 방지할 수 있습니다. 원자적 연산은 중간에 다른 스레드가 개입할 수 없도록 보장합니다.

Interlocked.Increment와 Decrement

Interlocked 클래스의 Increment와 Decrement 메서드는 다음과 같이 동작합니다:

  • Interlocked.Increment(ref number): number 변수를 원자적으로 증가시킵니다.
  • Interlocked.Decrement(ref number): number 변수를 원자적으로 감소시킵니다.

이를 사용하면 경합 조건을 방지할 수 있습니다.

원자성(Atomicity)과 경합 조건

원자성(atomicity)이란 연산이 더 이상 쪼갤 수 없는 하나의 단위로 이루어진다는 의미입니다. 원자적 연산은 중간에 다른 연산이 끼어들 수 없으므로, 여러 스레드가 동시에 실행되더라도 안전하게 공유 자원에 접근할 수 있습니다.

수정된 코드

Interlocked 클래스를 사용하여 경합 조건을 방지한 수정된 코드는 다음과 같습니다:

class Program
{
    // 경합 조건
    static int number = 0;

    static void Thread_1()
    {
        for (int i = 0; i < 10000; i++)
            Interlocked.Increment(ref number);
    }
    static void Thread_2()
    {
        for (int i = 0; i < 10000; 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);
    }
}

이제 두 스레드는 number 변수를 원자적으로 증가 및 감소시킵니다. 따라서 최종 결과는 항상 0이 됩니다.

결론

이번 포스트에서는 C#에서 멀티스레딩 프로그래밍 시 발생할 수 있는 경합 조건 문제와 이를 해결하기 위한 Interlocked 클래스의 사용에 대해 알아보았습니다. 경합 조건은 멀티스레드 환경에서 예상치 못한 결과를 초래할 수 있으며, 이를 방지하기 위해 원자적 연산을 사용하는 것이 중요합니다. Interlocked 클래스의 Increment와 Decrement 메서드를 사용하면 원자성을 보장하여 안전한 멀티스레드 프로그램을 작성할 수 있습니다.

멀티스레딩 프로그래밍은 복잡하지만, 올바른 동기화 기법을 사용하면 안정적이고 효율적인 코드를 작성할 수 있습니다. 추가적인 질문이나 궁금한 점이 있다면 언제든지 댓글로 남겨주세요!

반응형