CodeGym /행동 /C# SELF /다른 문제들: Livelock과 Starvation

다른 문제들: Livelock과 Starvation

C# SELF
레벨 57, 레슨 3
사용 가능

1. 소개

Livelock은 멀티스레드 애플리케이션에서 두 개 이상 스레드가 서로의 교착을 피하려고 시도하다가 결국 서로의 동작에 계속 반응만 하고 앞으로 나아가지 못하는 상황이에요. Deadlock과 달리 스레드들이 멈춰서 아무것도 못 하는 게 아니라, 프로그램은 살아있는 것처럼 보입니다: 스레드들은 실행되지만 어떤 유의미한 일을 하지 못해요.

이건 마치 좁은 복도에서 동료와 서로 비키려고 계속 뒤로 물러나서 결국 둘 다 앞으로 나아가지 못하는 상황과 같아요. 옆에서 보면 웃기지만 프로그램에선 끔찍하죠.

Livelock 예시: 문 앞의 프로그래머들

매우 예의 바른 두 프로그래머가 좁은 복도에서 서로 마주치며 걸어온다고 상상해보세요. 둘은 동시에 문 앞에 서 있고 둘 다 양보하기로 결정합니다: 비켜서서 상대를 먼저 보내주려고 하죠. 둘 다 상황이 변하지 않았다고 보고 또 양보하고 — 이게 무한 반복됩니다. 아무도 가만히 있진 않지만 문을 통과하는 사람도 없습니다.

멀티스레드 프로그램에서 이건 이렇게 보입니다: 스레드는 리소스가 점유된 걸 보고 물러나고, 다시 확인하고, 또 양보하고 — 끝없이요.

2. 실습으로 본 Livelock

비슷한 상황을 코드로 만들어볼게요. 예를 들어 두 개의 리소스(은행 계좌 두 개)가 있고, 두 스레드가 동시에 서로에게 돈을 이체하려고 하면서 "끼어들지 않으려고" 애쓰는 경우입니다.

class Account
{
    public int Balance { get; set; }
    public object LockObj { get; } = new object();
}

public class Program
{
    static void Transfer(Account from, Account to, int amount)
    {
        while (true)
        {
            bool lockedFrom = false;
            bool lockedTo = false;
            try
            {
                Monitor.TryEnter(from.LockObj, 100, ref lockedFrom); 
                Monitor.TryEnter(to.LockObj, 100, ref lockedTo);

                if (lockedFrom && lockedTo)
                {
                    from.Balance -= amount;
                    to.Balance += amount;
                    break; // 이체 완료!
                }
            }
            finally
            {
                if (lockedFrom) Monitor.Exit(from.LockObj);
                if (lockedTo) Monitor.Exit(to.LockObj);
            }
            // 안 됐나? 물러나고 다시 시도 — 예의 바르게!
            Thread.Sleep(1);
        }
    }
    // ... Transfer(A, B, ...); Transfer(B, A, ...);로 두 스레드 실행
}

이 예시에서 두 스레드는 반복해서 락을 얻으려 합니다. 그런데 둘 다 항상 락이 점유되어 있다고 보고 양보하면 결국 "예의 바른" 양보만 반복되어 돈이 한 번도 옮겨지지 않을 수 있어요. 스케줄러의 우연에만 의존하는 건 신뢰성이 떨어집니다. 랜덤 대기와 backoff를 넣고, Monitor.TryEnter와 Thread.Sleep로 짧은 빈 루프를 반복하지 마세요.

Livelock vs Deadlock 비교

Deadlock Livelock
스레드 멈춰서 대기 활성적으로 돌지만 멈춰있음
리소스 영구적으로 잠김 명시적으로 잠기지 않음
CPU 거의 사용 안 됨 100%까지 사용할 수 있음
해결책 타임아웃, 락 순서 정하기 랜덤성, 대기, backoff

3. Starvation (기아): 요청이 리소스에 도달하지 못할 때

Starvation(직역하면 "기아")은 한두 개 이상의 스레드가 지속적으로 소외되어 필요한 리소스에 접근하지 못하는 상황이에요. 다른 스레드들이 항시 먼저 가버리기 때문이죠.

Deadlock이나 Livelock과 달리 여기선 아무도 영원히 막힌 건 아니고, 아무도 계속 양보만 하는 건 아니에요 — 단지 특정 스레드들이 계속해서 "케이크 조각"을 못 받는 겁니다.

실생활 비유

직원 전용 계산대("VIP")와 일반 줄이 있는 식당을 생각해보세요. VIP 줄은 짧지만 항상 누군가 먼저 가요. 일반 손님들은 30분씩 서서 VIP가 계속 앞서가는 걸 보죠. 이게 바로 Starvation입니다!

4. C#에서의 Starvation: 실습과 디버깅

우선순위가 높은 스레드가 지속적으로 크리티컬 섹션에 진입해 낮은 우선순위 스레드가 거의 들어오지 못하는 lock 상황을 봅시다:

private static readonly object _locker = new object();

static void Greedy()
{
    while (true)
    {
        lock (_locker)
        {
            Console.WriteLine("욕심쟁이 스레드가 리소스를 잡았음...");
            Thread.Sleep(10); // 더 오래 락을 잡고 있음
        }
        Thread.Sleep(1);
    }
}

static void Poor()
{
    while (true)
    {
        lock (_locker)
        {
            Console.WriteLine("가난한 스레드가 들어가 보려 했음...");
        }
    }
}

두 스레드를 동시에 실행하면 "Greedy"(욕심쟁이)가 오래 락을 잡고 다시 빨리 잡아버려서 "Poor"(가난한)는 거의 내부에 들어가지 못하고 리소스에 대해 기아 상태가 됩니다. 실제 상황에서는 우선순위가 낮거나 OS가 자주 스레드를 선점하는 경우 등으로 더 미묘하게 나타날 수 있어요.

어디서 Starvation이 발생하나?

  • 우선순위가 낮은 스레드에서.
  • 작업 큐가 공정하지 않게 설계된 경우(FIFO 정책 부재).
  • ReaderWriterLockSlim의 기본 설정: 쓰기 작업자가 드문데 읽기가 끊임없이 오면, 쓰기자는 계속 기아 상태에 빠집니다. 읽기들이 Read Lock을 점유해서 Write Lock을 얻지 못하게 되거든요.

5. Starvation는 항상 버그는 아니지만 문제다

Starvation은 Deadlock처럼 프로그램을 완전히 멈추게 하진 않지만, 결과는 불공정하고 예측 불가능해집니다: 데이터 처리가 고르지 못하고, 일부 작업은 무한히 기다리며 성능이 저하됩니다. 특히 서버 애플리케이션에서는 모든 작업이 공평한 기회를 가져야 하므로 치명적일 수 있어요.

Livelock과 Starvation 진단 방법

  • CPU 사용량이 크게 올라갔지만 작업은 진전이 없다면 Livelock일 가능성이 큽니다.
  • 스레드들은 돌아가고 있지만 일부 작업이 절대 끝나지 않는다면 Starvation일 가능성이 높아요.
  • 로그: 특정 스레드나 작업이 리소스를 절대 얻지 못하는 기록이 있으면 확실한 기아 상태입니다.

Livelock을 피하는 법

  • Monitor.TryEnter 같은 "양보하는" 논블로킹 시도에만 의존하지 마세요. 실패하면 랜덤 대기(Thread.Sleep(Random.Next(1, 10)))) 같은 것을 넣어 스레드 타이밍이 겹치지 않게 하세요.
  • 짧은 빈 루프에서 계속 락을 시도하는 패턴을 피하세요 — "물러났다 다시 시도" 루프가 무한해질 수 있습니다.
  • 알고리즘을 바꿔 "연장자 권리" 같은 규칙을 도입하면 때때로 도움이 됩니다: 한쪽만 양보하게 하는 식으로요.

Starvation을 피하는 법

  • 스레드 우선순위를 관리하세요: 백그라운드 작업이나 우선순위 높은 작업이 다른 것들을 영원히 막지 않게 하세요.
  • 공정한 큐(ConcurrentQueue<T>, 채널, 작업 큐)를 사용해 작업이 들어온 순서대로 처리되게 하세요 (FIFO).
  • ReaderWriterLockSlim에서는 쓰기자를 위해 새로운 리더를 일시 중단시키는 전략을 도입할 수 있습니다, 그래야 쓰기자가 결국 Write Lock을 얻을 수 있어요.
  • 타임아웃과 로깅을 적용하세요: 스레드가 락을 얻으려 너무 오래 시도하면 경고를 남기게 하세요.
  • 락을 잡는 시간을 줄이고 크리티컬 섹션을 분리하세요.

Deadlock, Livelock, Starvation 비교

시나리오 Deadlock Livelock Starvation
스레드 완전히 멈춤 헛되이 돌아다님 누군긴 일하고, 누군긴 못 함
리소스 접근 불가 불가 불균형, 때론 불가
CPU 부하 적음 부하 큼 부하 있으나 모두는 아님
치명적 오류? 예 예 때때로
2
과제
C# SELF, 레벨 57, 레슨 3
잠금
Starvation 데모
Starvation 데모
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION