언어/C#

CLR 스레딩2 단순 계산 작업

tsyang 2022. 4. 17. 04:13

2022.04.10 - [언어/C#] - CLR 스레드 기본

 

CLR 스레드 기본

윈도우 스레드 도입 응용 프로그램의 인스턴스는 '프로세스' 라고 부르는 공간 내에서 수행된다. 개별 프로세스는 자신만의 가상 주소 공간을 가지고 있어서 다른 프로세스가 자신의 코드나 데

tsyang.tistory.com

 

CLR의 스레드에 대해 깊게 판다기 보다는 설계 아이디어에 초점을 맞춤

 

스레드 풀


이전 글에서 언급하였듯, 스레드를 생성하고 파괴하는 일은 상당한 시간을 소비한다. 또한 스레드를 과도하게 생성하면 잦은 컨텍스트 스위칭이 발생하여 성능에 악영향을 미친다. (+ 메모리 차지는 덤)

 

이런 상황을 위해 CLR은 고유의 스레드 풀을 관리하는 코드를 가지고 있다. 스레드풀은 CLR별로 하나씩 생성된다.

 

CLR이 초기화 되는 시점에 스레드 풀에 어떤 스레드도 존재하지 않는다. 스레드 풀은 내부적으로 작업 요청을 수신하는 큐를 가지고 있다. 작업이 들어오고 스레드가 없다면 스레드를 새로 생성한다. 스레드가 부족하다고 (보수적으로)판단해도 새로 만든다. 스레드가 작업을 완료하면 파괴되는 것이 아니라 스레드 풀로 반납되어 유휴 상태를 유지한다.

 

만약 스레드가 오랫동안 유휴 상태에 머물러 있는 경우, 스레드 풀은 해당 스레드를 깨워 자기 자신을 종료하도록 한다. 이 과정은 성능에 좋지 않은 영향을 미치지만, 스레드가 오랫동안 유휴 상태에 머물렀다는 것은 응용프로그램에 여유가 있다는 뜻이므로 크게 문제가 되지 않는다.

 

 

스레드 풀의 제약

CLR은 스레드 풀의 최대 갯수를 설정할 수 있도록 허용해주고 있다. 하지만 기아 현상이나 데드락이 발생할 수 있기 때문에 스레드 풀 내의 스레드 수의 상한을 설정할 수 없다. 예를 들어, N개의 작업이 큐잉 되어 있는데 모두 N+1번째 작업의 시그널을 기다리고 있다면? 그런데 스레드가 N개뿐이라면? 데드락에 빠지게 된다.

 

이런 이유로 스레드 풀이 가질 수 있는 최대 스레드 갯수의 기본값은 꾸준히 증가해왔다. 그럼에도 많은 스레드를 생성하는 것은 자원의 낭비가 크며 이상적인 스레드의 개수는 컴퓨터 내의 CPU의 개수와 같다.

 


 

단순 계산 중심 작업


using System.IO;
using System;
using System.Threading;

public static class Program
{
    public static void Main()
    {
        Console.WriteLine("메인 스레드 시작");
        ThreadPool.QueueUserWorkItem(SomeWork, "상태를 나타내는 데이터");

        Thread.Sleep(1000); //1초간 쉰다.
        Console.WriteLine("메인 스레드 작업 끝");
    }

    private static void SomeWork(object state)
    {
        Console.WriteLine($"새로운 스레드 state : {state}");
        Thread.Sleep(1000); //1초간 쉰다.
        Console.WriteLine("새로운 스레드 작업 끝");
    }
}

 

위 코드는 

 

메인 스레드 시작
새로운 스레드 state : 상태를 나타내는 데이터
메인 스레드 작업 끝
새로운 스레드 작업 끝

혹은

메인 스레드 시작
새로운 스레드 state : 상태를 나타내는 데이터
새로운 스레드 작업 끝
메인 스레드 작업 끝

를 출력한다. 즉, 비동기적으로 작업이 이뤄지고 있다.

 

ThreadPool.QueueUserWorkItem(WaitCallback callback, object state);

위 메서드가 스레드풀의 작업 큐에 작업을 넣는 메서드이다. state는 상태를 나타내는 데이터이다.

 

delegate void WaitCallBack(object state);

WaitCallback 델리게이트는 위처럼 선언되어있다. 즉, void를 반환하고 object를 매개변수로 받는 함수를 전달할 수 있다.

 

 


 

실행 컨텍스트


 

모든 스레드는 각자 실행 컨텍스트(Execution Contexts)라는 데이터 구조체를 가지고 있다. 여기에는 보안 설정, 호스트 설정, 논리 호출 컨텍스트 데이터라는게 있다고 한다. 다른 스레드에 일을 줄 때 이런 실행 컨텍스트가 다른 스레드로 전달(복사)된다. 그러나 또 다시 스레드가 다른 스레드에게 일을 주는 등의 경우 때문에 이 과정은 상당히 시간을 소비할 수 있다.

 

참고로 실행 컨텍스트의 전달을 제어하는 클래스(ExecutionContext)가 있다.

 


 

스레드 작업 취소


장기간 수행이 되는 계산 중심 작업은 취소기능을 제공하는 것이 좋다. 

 

이를 위해 닷넷 프레임워크는 작업 취소를 위한 표준화된 패턴을 제공한다. 이것을 협조적 취소 패턴(Cooperative Cancellation)이라고 한다.

 

코드는 다음과 같다.

using System;
using System.Threading;

public static class Program
{
    public static void Main()
    {
        var cts = new CancellationTokenSource();

        Console.WriteLine("엔터를 눌러 실행 취소");

        ThreadPool.QueueUserWorkItem(obj => Count(cts.Token, 1000));
        Console.ReadLine();

        cts.Cancel();   //토큰을 종료로 변경

        Console.ReadLine();
    }

    //약 1초간격으로 숫자 출력
    private static void Count(CancellationToken token, int countTo)
    {
        for (int i = 1; i <= countTo; ++i)
        {
            if (token.IsCancellationRequested)
            {
                Console.WriteLine("작업 취소됨");
                break;
            }

            Console.WriteLine(i);
            Thread.Sleep(1000);
        }

        Console.WriteLine("작업 종료");
    }
}

 

출력

엔터를 눌러 실행 취소
1
2
3
4

작업 취소됨
작업 종료

CancellationTokenSourceCancellationToken을 활용해 다른 스레드에서 수행하는 코드를 제어하고 있다.

 

혹은 CancelAfter()메서드를 이용해 일정 시간이 끝나고 정지하도록 할 수도 있다.

public static class Program
{
    public static void Main()
    {
        var cts = new CancellationTokenSource();

        Console.WriteLine("메인 스레드 : 작업 실행");

        ThreadPool.QueueUserWorkItem(obj => Count(cts.Token, 1000));
        cts.CancelAfter(3500); // 3.5초뒤 정지

        Console.ReadLine();
    }

    //약 1초간격으로 숫자 출력
    private static void Count(CancellationToken token, int countTo)
    {
        for (int i = 1; i <= countTo; ++i)
        {
            if (token.IsCancellationRequested)
            {
                Console.WriteLine("작업 취소됨");
                break;
            }

            Console.WriteLine(i);
            Thread.Sleep(1000);
        }

        Console.WriteLine("작업 종료");
    }
}

 

 만약 작업을 취소할 수 없도록 하려면 CancellationToken.None을 넘겨주면 된다. (static property)

 

CancellationTokenSource에는 Register 메서드가 있어 작업이 취소될 때 수행할 메서드를 등록할 수도 있다.

 

 


참고 : 제프리 리처, CLR via C# 4판 , 비제이퍼블릭, 27장. 계산 중심의 비동기 작업  [804~813, 842~845p]

'언어 > C#' 카테고리의 다른 글

CLR 단순 동기화1  (0) 2022.05.01
CLR 스레딩3 - 태스크(Task)  (0) 2022.04.24
CLR 스레딩1 기본  (0) 2022.04.10
런타임 Serialization - 1  (1) 2022.04.03
C# - 리플렉션 (Reflection)  (0) 2021.07.04