.NET 멀티스레딩 프로그래밍 핵심

스레드의 기본 개념

스레드는 프로그램 실행의 기본 단위입니다. 하나의 프로세스는 여러 개의 스레드로 구성될 수 있습니다. 각 스레드는 예외 처리기, 스케줄링 우선순위, 그리고 시스템이 스레드를 스케줄하기 전에 스레드 컨텍스트를 저장하는 데 사용하는 구조체 집합을 관리합니다. 스레드 컨텍스트는 스레드가 프로세스 주소 공간 내에서 원활하게 실행을 계속할 수 있도록 필요한 모든 정보를 포함하며, 여기에는 CPU 레지스터 세트와 스택이 포함됩니다.

분산 프로그래밍에서 스레드를 적절히 사용하면 애플리케이션의 성능과 실행 효율을 크게 향상시킬 수 있습니다. 원리는 하나의 프로세스를 여러 스레드로 분할하여 비동기적으로 동시에 실행함으로써 효율성을 높이는 것입니다. 동시 실행은 실제 동시 실행(CPU 점유)을 의미하지 않으며, 어떤 시점에서든 여전히 하나의 스레드만 CPU를 점유할 수 있습니다. 단지 CPU를 더 자주 차지하기 위해 경쟁하여 마치 모두 실행되는 것처럼 느껴지는 것입니다.

스레드를 사용하는 적절한 시기

일반적으로 여러 스레드가 실행될 때 특정 리소스 하나 또는 몇 개를 점유하려고 할 경우, 비동기 스레드 실행을 사용하지 않는 것이 좋습니다. 이들은 동시에 실행되므로 동시에 특정 리소스를 경쟁할 가능성이 높으며, 이때 리소스 할당 알고리즘을 실행하거나(예: 어떤 스레드의 우선순위가 높은지 판단하는 데 시간이 소요됨) 타임 슬라이스 알고리즘을 사용하게 됩니다(이는 CPU 순환/교대/양보에 필요한 시간을 소모함).

여러 스레드가 필요로 하는 시스템 리소스가 비교적 균일하게 분배될 경우, 비동기적인 동시 실행을 완전히 허용할 수 있습니다.

스레드 사용의 단점

  • 시스템은 프로세스와 스레드에 필요한 컨텍스트 정보를 저장하기 위해 메모리를 사용합니다. 따라서 생성할 수 있는 프로세스, AppDomain 객체 및 스레드의 수는 사용 가능한 메모리에 제한을 받습니다.
  • 많은 수의 스레드를 추적하는 것은 상당한 프로세서 시간을 소비합니다. 스레드가 너무 많으면 대부분의 스레드가 눈에 띄는 진행을 보이지 않을 수 있습니다. 현재 스레드의 대부분이 하나의 프로세스에 있는 경우, 다른 프로세스의 스레드 스케줄링 빈도는 낮아집니다.
  • 많은 스레드를 사용하여 코드 실행을 제어하는 것은 매우 복잡하며 많은 오류를 발생시킬 수 있습니다.
  • 스레드를 종료하려면 발생할 수 있는 문제를 이해하고 이러한 문제를 처리해야 합니다.

스레드 풀

많은 애플리케이션이 이벤트 발생을 기다리며 휴면 상태에서 많은 시간을 소비하는 스레드를 생성합니다. 이는 리소스 낭비입니다. 스레드 풀은 시스템이 관리하는 보조 스레드 풀을 애플리케이션에 제공하여 스레드를 더 효율적으로 사용할 수 있게 합니다. 하나의 스레드가 스레드 풀에 대기 중인 여러 작업의 상태를 모니터링합니다. 대기 작업이 완료되면 스레드 풀의 보조 스레드가 해당 콜백 함수를 실행합니다.

실제로 여러 스레드가 필요한 짧은 작업을 실행해야 하는 경우, ThreadPool 클래스를 사용하는 것이 여러 스레드를 활용하는 가장 편리하고 좋은 방법입니다. 스레드 풀을 사용하면 시스템이 이 프로세스뿐만 아니라 컴퓨터의 다른 프로세스(애플리케이션이 인식하지 못하는)에 대해 이러한 상황을 최적화하여 더 나은 처리량을 달성할 수 있습니다. 스레드 풀을 사용하면 시스템이 컴퓨터의 모든 현재 프로세스를 고려하여 스레드 시간 조각을 최적화할 수 있습니다.

ThreadPool 클래스

스레드 풀은 ThreadPool 클래스의 인스턴스가 처음 생성될 때 생성됩니다. 스레드 풀은 사용 가능한 프로세서당 기본 제한으로 25개의 스레드를 가집니다.

대기 작업과 관련 없는 작업 항목을 스레드 풀에 등록할 수 있습니다. 스레드 풀의 스레드가 작업 항목을 처리하도록 요청하려면 QueueUserWorkItem 메서드를 호출합니다. 이 메서드는 스레드 풀에서 선택된 스레드가 호출할 메서드 또는 대리자에 대한 참조를 매개변수로 사용합니다. 작업 항목이 큐에 등록된 후에는 취소할 수 없습니다.

스레드 생성 및 종료

using System;
using System.Threading;

public class TaskProcessor
{
    public void ProcessTask()
    {
        while (!_terminationFlag)
        {
            Console.WriteLine("백그라운드 스레드: 작업 중...");
        }
        Console.WriteLine("백그라운드 스레드: 정상적으로 종료됨.");
    }
    public void SignalTermination()
    {
        _terminationFlag = true;
    }
    private volatile bool _terminationFlag;
}

public class ThreadCreationDemo
{
    static void Main()
    {
        TaskProcessor processor = new TaskProcessor();
        Thread backgroundThread = new Thread(processor.ProcessTask);

        backgroundThread.Start();
        Console.WriteLine("메인 스레드: 백그라운드 스레드 시작 중...");

        while (!backgroundThread.IsAlive);

        Thread.Sleep(1);

        processor.SignalTermination();

        backgroundThread.Join();
        Console.WriteLine("메인 스레드: 백그라운드 스레드가 종료됨.");
    }
}

스레드 풀 활용

using System;
using System.Threading;

public class MathTask
{
    public MathTask(int value, ManualResetEvent completionSignal)
    {
        InputValue = value;
        CompletionEvent = completionSignal;
    }

    public void ExecuteInPool(Object context)
    {
        int taskId = (int)context;
        Console.WriteLine($"작업 {taskId} 시작됨...");
        ResultValue = ComputeFactorial(InputValue);
        Console.WriteLine($"작업 {taskId} 계산 완료...");
        CompletionEvent.Set();
    }

    public int ComputeFactorial(int n)
    {
        if (n <= 1) return 1;
        return n * ComputeFactorial(n - 1);
    }

    public int InputValue { get; private set; }
    public int ResultValue { get; private set; }
    private ManualResetEvent CompletionEvent;
}

public class ThreadPoolDemo
{
    static void Main()
    {
        const int TotalTasks =แก่
        ManualResetEvent[] taskEvents = new ManualResetEvent[TotalTasks];
        MathTask[] tasks = new MathTask[TotalTasks];
        Random randGen = new Random();

        Console.WriteLine($"{TotalTasks}개 작업 시작 중...");
        for (int i = 0; i < TotalTasks; i++)
        {
            taskEvents[i] = new ManualResetEvent(false);
            tasks[i] = new MathTask(randGen.Next(5, 10), taskEvents[i]);
            ThreadPool.QueueUserWorkItem(tasks[i].ExecuteInPool, i);
        }

        WaitHandle.WaitAll(taskEvents);
        Console.WriteLine("모든 계산 완료.");

        for (int i = 0; i < TotalTasks; i++)
        {
            Console.WriteLine($"팩토리얼({tasks[i].InputValue}) = {tasks[i].ResultValue}");
        }
    }
}

스레드 동기화와 상호작용

using System;
using System.Threading;
using System.Collections.Generic;

public class CoordinationSignals
{
    public CoordinationSignals()
    {
        ItemReady = new AutoResetEvent(false);
        ShutdownSignal = new ManualResetEvent(false);
        SignalArray = new WaitHandle[] { ItemReady, ShutdownSignal };
    }

    public EventWaitHandle ShutdownSignal { get; private set; }
    public EventWaitHandle ItemReady { get; private set; }
    public WaitHandle[] SignalArray { get; private set; }
}

public class ItemProducer
{
    public ItemProducer(Queue<int> storage, CoordinationSignals signals)
    {
        StorageQueue = storage;
        SyncSignals = signals;
    }
    
    public void ProductionLoop()
    {
        int totalProduced = 0;
        Random rand = new Random();
        
        while (!SyncSignals.ShutdownSignal.WaitOne(0, false))
        {
            lock (StorageQueue)
            {
                while (StorageQueue.Count < 15)
                {
                    StorageQueue.Enqueue(rand.Next(50, 150));
                    SyncSignals.ItemReady.Set();
                    totalProduced++;
                }
            }
        }
        Console.WriteLine($"생산자 스레드: {totalProduced}개 항목 생성됨");
    }
    
    private Queue<int> StorageQueue;
    private CoordinationSignals SyncSignals;
}

public class ItemConsumer
{
    public ItemConsumer(Queue<int> storage, CoordinationSignals signals)
    {
        StorageQueue = storage;
        SyncSignals = signals;
    }
    
    public void ConsumptionLoop()
    {
        int itemsConsumed = 0;
        while (WaitHandle.WaitAny(SyncSignals.SignalArray) != 1)
        {
            lock (StorageQueue)
            {
                if (StorageQueue.Count > 0)
                {
                    int item = StorageQueue.Dequeue();
                    itemsConsumed++;
                }
            }
        }
        Console.WriteLine($"소비자 스레드: {itemsConsumed}개 항목 처리됨");
    }
    
    private Queue<int> StorageQueue;
    private CoordinationSignals SyncSignals;
}

public class SynchronizationExample
{
    private static void DisplayQueue(Queue<int> q)
    {
        lock (q)
        {
            foreach (int val in q)
            {
                Console.Write($"{val} ");
            }
        }
        Console.WriteLine();
    }

    static void Main()
    {
        CoordinationSignals sync = new CoordinationSignals();
        Queue<int> sharedQueue = new Queue<int>();

        Console.WriteLine("작업 스레드 구성 중...");
        ItemProducer producer = new ItemProducer(sharedQueue, sync);
        ItemConsumer consumer = new ItemConsumer(sharedQueue, sync);

        Thread producerThread = new Thread(producer.ProductionLoop);
        Thread consumerThread = new Thread(consumer.ConsumptionLoop);

        Console.WriteLine("생산자 및 소비자 스레드 시작...");
        producerThread.Start();
        consumerThread.Start();

        for (int i = 0; i < 3; i++)
        {
            Thread.Sleep(2000);
            DisplayQueue(sharedQueue);
        }

        Console.WriteLine("스레드 종료 신호 전송...");
        sync.ShutdownSignal.Set();

        Console.WriteLine("메인 스레드: 스레드 완료 대기 중...");
        producerThread.Join();
        consumerThread.Join();
    }
}

태그: C# Thread threadpool 동기화 멀티스레딩

10월 5일 00:47에 게시됨