C# 스레드와 태스크

김민구·2025년 5월 27일
0

C#

목록 보기
29/31

1. 프로세스와 스레드의 기초

  • 프로세스(Process): 실행 중인 프로그램을 의미합니다. 각 프로세스는 독립적인 메모리 공간을 가집니다. 마치 컴퓨터에서 실행되는 여러 개의 독립적인 애플리케이션과 같습니다.
  • 스레드(Thread): 프로세스 내에서 실행되는 실행 단위입니다. 하나의 프로세스는 하나 이상의 스레드를 가질 수 있습니다. 스레드는 프로세스의 메모리 공간을 공유합니다. 마치 한 애플리케이션 내에서 동시에 여러 기능을 수행하는 것과 같습니다.

2. 스레드 다루기: 생성, 시작, 대기, 종료

C#에서 스레드는 System.Threading.Thread 클래스를 사용하여 생성하고 관리할 수 있습니다.

  • 스레드 생성 및 시작: Thread 클래스의 인스턴스를 생성하고 Start() 메서드를 호출하여 스레드를 시작합니다. 스레드는 실행할 메서드를 인자로 받아 생성됩니다.
    static void DoSomething() { ... } // 스레드가 실행할 메서드
    Thread t1 = new Thread(new ThreadStart(DoSomething)); // Thread 인스턴스 생성
    t1.Start(); // 스레드 시작
  • 스레드 대기: 메인 스레드나 다른 스레드가 특정 스레드의 작업이 완료될 때까지 기다려야 할 때가 있습니다. 이럴 때는 Join() 메서드를 사용합니다. Join() 메서드는 해당 스레드가 실행을 마치고 완전히 정지할 때까지 호출한 스레드를 블록합니다. Thread.Sleep() 메서드는 인자로 주어진 시간(밀리초)만큼 스레드의 CPU 사용을 멈춥니다.
  • 스레드 임의 종료 (Abort): 스레드를 강제로 종료해야 할 경우 Abort() 메서드를 사용할 수 있습니다. Abort()가 호출되면 해당 스레드에서 ThreadAbortException 예외가 발생하며, 이를 통해 스레드의 실행을 중단시킬 수 있습니다. 스레드가 Abort()에 의해 정지될 때까지 Join() 메서드로 대기할 수 있습니다. 하지만 Abort()는 권장되지 않는 방식입니다.
  • 스레드 중단 (Interrupt): 스레드가 Sleep(), Wait(), Join()과 같은 대기 상태에 있을 때, Interrupt() 메서드를 사용하여 스레드를 깨울 수 있습니다. Interrupt()가 호출되면 해당 스레드에서 ThreadInterruptedException 예외가 발생합니다. Interrupt()는 스레드를 강제로 종료하기보다는, 대기 상태에서 벗어나게 하여 정상적인 종료 절차를 수행하도록 유도할 때 사용될 수 있습니다. Interrupt() 호출 후 스레드가 정지할 때까지 Join()으로 대기할 수 있습니다.

3. 스레드 상태 변화

스레드는 여러 상태를 가집니다. 주요 상태 변화는 다음과 같습니다:

  • Unstarted: 스레드가 생성되었지만 아직 시작되지 않은 상태.
  • Running: 스레드가 실행 중인 상태. Thread.Start()에 의해 이 상태로 진입합니다.
  • WaitSleepJoin: Monitor.Wait(), Thread.Sleep(), Thread.Join() 메서드에 의해 일시 중단된 상태.
  • Suspended: Thread.Suspend()에 의해 일시 중단된 상태 (권장되지 않음).
  • Stopped: 스레드가 실행을 완료하거나 종료되어 정지된 상태.
  • AbortRequested: Thread.Abort()가 호출되어 스레드 종료가 요청된 상태.
  • Aborted: Thread.Abort()에 의해 강제로 종료된 상태.

ThreadState 열거형을 통해 스레드의 상태를 확인할 수 있습니다.

4. 스레드 간 동기화

여러 스레드가 하나의 공유 자원(예: 변수, 파일)에 동시에 접근하여 데이터를 변경하려 할 때 문제가 발생할 수 있습니다. 이를 경쟁 상태(Race Condition)라고 하며, 예상치 못한 결과가 나올 수 있습니다. 이러한 문제를 해결하기 위해 스레드 간 동기화(Synchronization)가 필요합니다.

  • lock 키워드: C#에서 가장 간단한 동기화 방법은 lock 키워드를 사용하는 것입니다. lock 블록으로 감싼 코드는 한 번에 하나의 스레드만 접근할 수 있도록 보장합니다. lock은 내부적으로 Monitor 클래스를 사용합니다.
    private readonly object thisLock = new object(); // 동기화에 사용할 객체
    public void Increase()
    {
        lock (thisLock) // lock 블록
        {
            count = count + 1; // 이 코드는 한 번에 하나의 스레드만 실행
        }
    }
    lock 블록이 끝날 때까지 다른 스레드는 해당 코드를 실행할 수 없습니다.
  • Monitor 클래스: lock 키워드보다 더 세밀한 제어가 필요할 때 Monitor 클래스를 직접 사용할 수 있습니다. Monitor.Enter()로 진입하고 Monitor.Exit()으로 빠져나오며 임계 영역을 설정합니다. Monitor.Wait()Monitor.Pulse()/PulseAll()을 사용하여 스레드 간에 신호를 주고받으며 대기 상태를 제어할 수도 있습니다.

5. 태스크(Task)와 병렬(Parallel) 처리

.NET Framework 4.0부터 도입된 태스크(Task)는 스레드보다 고수준의 추상화이며, 비동기 및 병렬 작업을 더 쉽게 관리할 수 있도록 설계되었습니다. 대부분의 경우 Thread 클래스보다 Task를 사용하는 것이 권장됩니다.

  • Task 클래스: Task는 반환 값이 없는 비동기 작업을 표현합니다. Task.Run() 메서드를 사용하여 간단하게 작업을 백그라운드에서 실행할 수 있습니다. Task.Wait() 메서드는 해당 태스크가 완료될 때까지 기다립니다.
  • Task 클래스: Task<TResult>는 반환 값이 있는 비동기 작업을 표현합니다. 작업이 완료된 후 Result 속성을 통해 결과를 얻을 수 있습니다. Result에 접근하면 작업이 완료될 때까지 대기하게 됩니다.
  • Parallel 클래스: Parallel 클래스는 병렬 처리를 쉽게 수행할 수 있도록 돕습니다. Parallel.For()Parallel.ForEach()와 같은 메서드를 통해 반복 작업을 여러 스레드에서 병렬로 실행하여 성능을 향상시킬 수 있습니다. 병렬 처리는 하나의 작업을 여러 작업자로 나누어 동시에 수행하는 방식입니다.

6. async 및 await를 사용한 비동기 코드

asyncawait 키워드는 비동기 코드를 작성하는 새로운 방식입니다. 주로 I/O 바운드 작업(파일 읽기/쓰기, 네트워크 통신 등)에서 메인 스레드를 블록하지 않고 다른 작업을 수행할 수 있도록 합니다.

  • async 키워드: 메서드 선언에 사용하여 해당 메서드가 비동기 메서드임을 나타냅니다. async 메서드는 일반적으로 Task 또는 Task<TResult>를 반환합니다.
  • await 키워드: async 메서드 내에서 비동기 작업(Task 또는 Task<TResult> 반환)의 완료를 기다릴 때 사용합니다. await를 만나면 해당 메서드의 실행은 일시 중단되고, 호출 스레드는 다른 작업을 수행할 수 있게 됩니다. 비동기 작업이 완료되면 중단되었던 메서드의 실행이 재개됩니다.

Task.Delay()Thread.Sleep()과 유사하게 지정된 시간만큼 대기하지만, 호출 스레드를 블록하지 않는다는 중요한 차이가 있습니다. 따라서 UI 스레드에서 Task.Delay()를 사용해도 UI가 멈추지 않습니다. .NET은 ReadAsync, WriteAsync 등 다양한 비동기 API를 제공하며, 이들은 주로 I/O 바운드 작업을 처리합니다.

profile
C#, Unity

0개의 댓글