본문 바로가기

C#

C# Thread.Join과 Thread.Sleep 차이와 활용

Thread.Join과 Thread.Sleep은 모두 "기다림"을 만들지만 의도와 대상이 다릅니다. Join은 특정 스레드가 끝날 때까지 현재 스레드를 대기시키고, Sleep은 현재 스레드를 지정한 시간만큼 멈춥니다. 용도에 맞게 선택하는 것이 병목과 버그를 줄이는 핵심입니다.

1. 개념 정리

Thread.Sleep(ms)는 현재 스레드가 ms 동안 실행을 중단합니다. 다른 스레드의 완료 여부와는 무관하며 시간이 지나면 자동으로 깨어납니다.

Thread.Join()은 대상 스레드가 종료될 때까지 현재 스레드를 차단합니다. Join(int timeout) 또는 Join(TimeSpan timeout)으로 최대 대기 시간을 설정할 수 있습니다.

2. 핵심 차이

Sleep은 시간 기반 대기이며, Join은 완료 기반 대기입니다. Sleep은 현재 스레드만 멈추고, Join은 다른 스레드의 생명주기를 기준으로 멈춥니다. Sleep은 락을 해제하지 않으며 정확한 타이밍 보장을 하지 않습니다. Join은 안전하게 작업 종료를 동기화할 때 적합합니다.

3. 기본 예제: Sleep vs Join

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        var worker = new Thread(() =>
        {
            Console.WriteLine("작업 시작");
            Thread.Sleep(1500); // 실제 작업 시뮬레이션
            Console.WriteLine("작업 종료");
        });

        worker.Start();

        // Sleep으로 기다리기: 작업 종료를 보장하지 않음
        Thread.Sleep(1000);
        Console.WriteLine($"Sleep 후 worker 살아있음? {worker.IsAlive}");

        // Join으로 기다리기: 종료를 보장함
        worker.Join();
        Console.WriteLine($"Join 후 worker 살아있음? {worker.IsAlive}");
    }
}

4. Thread.Join 활용 패턴

메인 스레드가 여러 작업 스레드의 완료를 기다려 안전하게 리소스를 정리할 때 사용합니다. 타임아웃을 주어 시스템 응답성을 유지할 수 있습니다.

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        var t1 = new Thread(() => Thread.Sleep(500));
        var t2 = new Thread(() => Thread.Sleep(2000));
        t1.Start();
        t2.Start();

        // 타임아웃 Join: true면 정상 종료, false면 시간 초과
        bool done = t2.Join(1000);
        Console.WriteLine(done ? "t2 종료" : "t2 타임아웃");

        // 반드시 모든 스레드 종료 보장
        t1.Join();
        t2.Join();
        Console.WriteLine("정리 완료");
    }
}

5. Thread.Sleep 활용 패턴

폴링 간격 조절, 임시 백오프, 데모/테스트에서 단순 지연이 필요할 때 사용합니다. 종료 조건과 함께 사용하여 바쁜 대기를 피합니다.

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        int iterations = 5;
        while (iterations-- > 0)
        {
            DoPeriodicWork();
            Thread.Sleep(1000); // 1초 간격으로 주기 작업
        }
    }

    static void DoPeriodicWork()
    {
        Console.WriteLine($"Tick: {DateTime.Now:HH:mm:ss.fff}");
    }
}

6. 피해야 할 실수와 대안

Sleep으로 "조건 발생"을 기다리면 타이밍에 취약합니다. 신호(이벤트)로 대기하는 것이 더 안전합니다.

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        var ready = new ManualResetEventSlim(false);
        var producer = new Thread(() =>
        {
            Thread.Sleep(800); // 작업 후 신호
            ready.Set();
        });

        producer.Start();
        Console.WriteLine("신호 대기 중...");
        ready.Wait(); // 조건 충족 시 즉시 깨어남
        Console.WriteLine("계속 진행");
    }
}

또한 락 내부에서 Sleep을 호출하면 락이 유지되어 다른 스레드가 막힙니다. 가능하면 피합니다.

object gate = new object();
lock (gate)
{
    // 여기서 Sleep은 락을 해제하지 않음 - 경쟁 스레드 모두 대기
    Thread.Sleep(1000);
}

7. 현대 .NET에서는 Task/await 고려

CPU를 차단하지 않는 비동기 대기를 원하면 Task 기반 API를 사용하는 것이 좋습니다. Task.Delay는 스레드를 점유하지 않습니다. Thread.Join에 해당하는 동기화는 Task.WhenAll 또는 await로 표현합니다.

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        await Task.Delay(1000); // 비동기 지연, 스레드 차단 없음
        await Task.WhenAll(Task.Run(Work), Task.Run(Work));
        Console.WriteLine("모든 작업 완료");
    }

    static async Task Work()
    {
        await Task.Delay(500);
    }
}

8. 정리

현재 스레드를 일정 시간 멈춰 간격을 조절하려면 Sleep을 사용합니다. 다른 스레드의 종료를 정확히 기다리려면 Join을 사용합니다. 조건 대기는 이벤트/세마포어 등 동기화 프리미티브로 처리하고, 새로운 코드베이스에서는 가능하면 Task/await를 우선 고려합니다.