스레드 / 태스크와 프로세스 / Parallel 클래스

Coding Cat·2024년 7월 10일

C#

목록 보기
10/11

프로세스(Process)

실행파일의 데이터와 코드가 메모리에 적재된 인스턴스이다.

word.exe가 실행파일이라면, 이 실행 파일을 실행한 것이 프로세스이다.

프로세스란 실행파일이 실행되어 실행파일과 관련된 모든 데이터가 메모리에 적재되어 실행되는 상태를 말한다. 프로세스는 자신만의 스레드를 가질 수 있으며 이 스레드로 프로그램에 필요한 동작을 수행한다.

스레드(Thread)

운영체제가 CPU 시간을 할당하는 기본 단위이다. 하나의 프로세스 내에서 동시에 여러 작업을 실행하기 위해 만들어진 흐름이다.

프로세스는 최소 하나의 스레드를 포함하고 있으며, 이 스레드가 프로세스의 주요 작업을 수행한다. 멀티스레딩(Multithreading)은 하나의 프로세스 내에서 여러 스레드가 동시에 실행되는 것을 의미하며, 이를 통해 프로그램의 성능과 응답성을 향상시킬 수 있다.

스레드란 프로세스 내부에서 생성되는 실제로 작업을 하는 주체이다. 프로세스 안에 있는 스레드들은 코드, 데이터, 힙 영역은 공유하고 스택은 각각 따로 할당된다.

Thread 클래스의 메소드

메서드/속성설명예제 코드
Thread.Start()스레드를 시작thread.Start();
Thread.Join()스레드가 종료될 때까지 현재 스레드를 차단thread.Join();
Thread.Sleep(int millisecondsTimeout)지정된 시간(밀리초) 동안 현재 스레드를 일시 중지Thread.Sleep(1000); // 1초 동안 일시 중지
Thread.Abort()스레드를 강제로 종료 (사용 권장되지 않음)thread.Abort();
Thread.Interrupt()차단된 스레드를 인터럽트하여 ThreadInterruptedException을 발생시킴thread.Interrupt();
Thread.IsAlive스레드가 실행 중인지 여부를 나타내는 값을 가져옴bool isAlive = thread.IsAlive;
Thread.IsBackground스레드가 백그라운드 스레드인지 여부를 가져오거나 설정thread.IsBackground = true;
Thread.ManagedThreadId스레드의 관리되는 ID를 가져옴int threadId = thread.ManagedThreadId;
Thread.Name스레드의 이름을 가져오거나 설정thread.Name = "MyThread";
Thread.Priority스레드의 우선순위를 가져오거나 설정thread.Priority = ThreadPriority.Highest;
Thread.CurrentThread현재 실행 중인 스레드를 가져옴
using System;
using System.Threading;

class Program
{
    static void Main(string[] args)
    {
        // 새로운 스레드를 생성하고 시작할 준비를 합니다.
        Thread thread = new Thread(new ThreadStart(DoWork));
        
        // 스레드의 이름을 설정합니다. 이 이름은 디버깅 시 유용합니다.
        thread.Name = "WorkerThread";
        
        // 스레드의 우선순위를 설정합니다. 여기서는 가장 높은 우선순위로 설정합니다.
        thread.Priority = ThreadPriority.Highest;
        
        // 스레드를 시작합니다.
        thread.Start();

        // 메인 스레드에서 5번의 루프를 돌면서 메시지를 출력합니다.
        for (int i = 0; i < 5; i++)
        {
            // 메인 스레드에서 현재 루프 카운터를 출력합니다.
            Console.WriteLine($"Main thread: {i}");
            
            // 1초 동안 현재 스레드를 일시 중지합니다.
            Thread.Sleep(1000);
        }

        // 백그라운드 스레드가 완료될 때까지 현재 스레드를 차단합니다.
        thread.Join();
        
        // 백그라운드 스레드가 종료된 후에 이 메시지가 출력됩니다.
        Console.WriteLine("Main thread finished.");
    }

    static void DoWork()
    {
        // 백그라운드 스레드에서 5번의 루프를 돌면서 메시지를 출력합니다.
        for (int i = 0; i < 5; i++)
        {
            // 현재 스레드의 이름과 루프 카운터를 출력합니다.
            Console.WriteLine($"Background thread ({Thread.CurrentThread.Name}): {i}");
            
            // 1초 동안 현재 스레드를 일시 중지합니다.
            Thread.Sleep(1000);
        }
    }
}

//출력결과
/*Main thread: 0
Background thread (WorkerThread): 0
Main thread: 1
Background thread (WorkerThread): 1
Main thread: 2
Background thread (WorkerThread): 2
Main thread: 3
Background thread (WorkerThread): 3
Main thread: 4
Background thread (WorkerThread): 4
Main thread finished.*/

태스크(Task)

C#에서 병렬 처리나 비동기 작업을 수행하기 위해 제공하는 기능

System.Threading.Tasks 네임스페이스에 포함된 Task, Task<TResult>, Parallel 클래스

비동기 코드는 비동기 메소드를 호출하여 그 메소드가 종료되어 반환되기 전까지 기다리지 않고 계속 실행하는 것을 의미한다.

비동기의 반대, 동기 코드는 일반적인 실행 흐름과 같으며 한 라인이 실행되어야 다음 라인이 실행될 수 있다.

.NetFramework에서 도입된 비동기 작업을 추상화한 클래스이다. Task는 내부적으로 ThreadPool을 이용하여 스레딩을 관리하므로,개발자는 세부적인 작업을 신경 쓸 필요 없이 로직에 집중할 수 있다. 또한 async/await 패턴을 지원하여 비동기 프로그래밍을 쉽게 구현한다.

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        // Task.Run으로 비동기 작업 실행
        Task task1 = Task.Run(() =>
        {
            Console.WriteLine("Task 1 started.");
            Task.Delay(2000).Wait(); // 2초 대기
            Console.WriteLine("Task 1 completed.");
        });

        // Task.Delay로 비동기 대기
        await Task.Delay(1000); // 1초 대기

        // Task.FromResult로 완료된 Task 생성
        Task<int> task2 = Task.FromResult(42);

        // Task.WhenAll로 여러 Task 동시에 실행
        await Task.WhenAll(task1, task2);

        // Task.ContinueWith로 이전 Task 완료 후 작업 실행
        task2.ContinueWith(t => Console.WriteLine($"Task 2 result: {t.Result}"));

        Console.WriteLine("All tasks completed.");
    }
}

Task 클래스 메소드

메서드/속성설명예제 코드
Task.Run(Action action)주어진 작업(action)을 비동기적으로 실행합니다.Task.Run(() => { /* 작업 */ });
Task.Factory.StartNew(Action action)주어진 작업(action)을 비동기적으로 실행합니다.Task.Factory.StartNew(() => { /* 작업 */ });
Task.FromResult<TResult>(TResult result)지정된 결과 값으로 완료된 Task<TResult> 인스턴스를 생성합니다.Task<int> task = Task.FromResult(42);
Task.Delay(int millisecondsDelay)지정된 시간(밀리초) 동안 비동기적으로 대기합니다.await Task.Delay(1000); // 1초 동안 대기
Task.Wait()Task의 완료를 동기적으로 대기합니다.task.Wait();
Task.WhenAny(params Task[] tasks)여러 Task 중 하나가 완료될 때까지 기다립니다.Task completedTask = await Task.WhenAny(task1, task2, task3);
Task.WhenAll(params Task[] tasks)모든 Task가 완료될 때까지 기다립니다.await Task.WhenAll(task1, task2, task3);
Task.ContinueWith(Action<Task> continuationAction)이전 Task가 완료된 후에 추가 작업을 실행합니다.task.ContinueWith(t => Console.WriteLine($"Result: {t.Result}"));
Task.ResultTask가 완료될 때까지 대기한 후 결과를 반환합니다.int result = task.Result;
Task.StatusTask의 현재 상태를 가져옵니다.TaskStatus status = task.Status;
Task.IsCompletedTask가 완료되었는지 여부를 나타내는 값을 가져옵니다.bool isCompleted = task.IsCompleted;
Task.IsFaultedTask가 실패했는지 여부를 나타내는 값을 가져옵니다.bool isFaulted = task.IsFaulted;
Task.IsCanceledTask가 취소되었는지 여부를 나타내는 값을 가져옵니다.bool isCanceled = task.IsCanceled;

async와 await키워드

async 키워드는 메서드 선언부에 사용되며, 해당 메서드가 비동기적으로 실행될 수 있음을 나타낸다. 이 키워드는 다음과 같은 특성을 가진다.

  • 비동기 메서드 정의: 메서드가 async로 선언되면 메서드 내에서 await 키워드를 사용할 수 있다.
  • 컴파일러에 의해 특별 처리: async 메서드는 컴파일러에 의해 비동기 상태 머신(State Machine)으로 변환된다.
  • 반환 타입: async 메서드는 반환 타입으로 Task, Task<T> 또는 void를 사용할 수 있다. 대부분의 경우 Task나 Task<T>를 사용하여 비동기 작업의 결과를 반환한다.

await 키워드는 비동기 메서드 내에서 사용되는 키워드이다. 이 키워드는 Task, Task를 반환하는 메서드나 표현식 앞에 사용할 수 있다.

  • 비동기 작업 완료 대기: await 키워드는 비동기 작업이 완료될 때까지 현재 메서드의 실행을 일시 중지한다.
  • 비동기 작업 결과 추출: await 키워드는 비동기 작업의 결과를 추출하고, 필요에 따라 변수에 할당할 수 있다.
  • 비동기 작업 예외 처리: await 키워드를 사용하면 비동기 작업에서 발생하는 예외를 쉽게 처리할 수 있다.

이때 중요한 점은 await 키워드가 메서드의 실행을 일시 중지시킨다고 해도, 전체 프로그램의 실행을 중지시키는 것이 아니라는 점이다. 따라서 await 키워드를 사용하면 비동기 작업이 실행되는 동안에도 다른 작업을 실행할 수 있다.

참고 C# ConfigureAwait 메소드

Parallel 클래스

Parallel 클래스는 좀 더 쉽게 병렬처리를 하고 싶은 메소드를 처리할 수 있게 도와준다.

병렬 프로그래밍과 비동기 프로그래밍은 둘 다 동시에 여러 작업을 처리하는 프로그래밍 기법이지만, 병렬 프로그래밍은 비동기 프로그래밍이 아니다.

병렬 처리는 하나의 작업을 여러 개의 작업 단위로 나누어 여러 개의 스레드로 동시에 수행하는 것을 말한다. 하나의 작업 시간을 줄이기 위해 여러 개의 스레드를 동시에 실행하는 것이다.

lock 키워드

lock 문은 C#에서 멀티스레딩 환경에서 동기화된 코드 블록을 만들기 위해 사용된다. 여러 스레드가 동시에 같은 리소스에 접근하는 경우, 데이터의 일관성을 보장하기 위해 lock을 사용한다. lock 문을 사용하면 한 스레드가 리소스를 사용 중일 때 다른 스레드가 해당 리소스에 접근하지 못하게 한다.

lock (object)
{
    // 동기화된 코드 블록
}
/*object: 동기화에 사용되는 참조 타입의 객체이다.
  이 객체는 스레드 간에 공유되어야 한다. 
  일반적으로 private 필드로 선언된 object 타입의 변수가 사용된다.*/

lock 사용할 때 주의할 점

lock(this)
lock(typeof(클래스명))
->위와 같이 사용하는 것은 자제

예제

// 두개의 스레드는 하나의 멤버변수 num에 접근해 숫자를 하나 증가해주고 0.005초 후 출력을 해주는 예제
namespace Increase
{
    class Program
    {
        static void Main(string[] args)
        {
            Data myData = new Data();
            Thread myThread1 = new Thread(MyFunc);
            Thread myThread2 = new Thread(MyFunc);

            myThread1.Start(myData);
            myThread2.Start(myData);

            myThread1.Join();
            myThread2.Join();
        }

        private static void MyFunc(object obj)
        {
            Data targetData = obj as Data;

            for (int i = 0; i < 10; i++)
            {
                targetData.Increase();
                Console.WriteLine(targetData.num);
            }
        }
    }

    class Data
    {
        public int num = 0;

        public void Increase()
        {
            this.num++;
            Thread.Sleep(5);
        }
    }
}


첫번째 스레드는 num을 0에서 1로 변경해준후 0.005초 대기 후 출력
두번째 스레드는 num을 1에서 2로 변경해준후 0.005초 대기 후 출력
위의 과정을 두개의 스레드가 반복

그러나 첫번째 스레드가 num을 0에서 1로 변경후 0.005초 대기할 때 두번째스레드가 num을 1에서 2로 변경해준것이다. 그래서 1은 출력되지않고 2가 두번 출력된 상황이다.

해당오류는 "공유자원에 대한 스레드 동기화가 되지 않아서 생긴 오류"이다.

해결방법은 num에 1을 더해주고 0.005초 대기하는 로직을 한개의 스레드만 접근할 수 있도록 제어해줘야 한다.

class Data
{
    private object obj = new object();
    public int num = 0;

    public void Increase()
    {
    	lock (obj)
        {
            this.num++;
            Thread.Sleep(5);
        }
    }
}


이제 정상적으로 원하는결과가 출력된다.

자주 실수하는 예제

//메서드 안에서 object변수를 생성해서 lock이 걸리지 않는 예제
class Data
{ 
    public int num = 0;

    public void Increase()
    {
    	object obj = new object();
    	lock (obj)
        {
            this.num++;
            Thread.Sleep(5);
        }
    }
}

스레드들이 해당 메서드에 들어올때마다 object형 변수가 새롭게 생성되어 스레드동기화가 되지 않는다. object변수는 한번만 생성되게 멤버변수로 처리해주는게 일반적이다.

추가로 씨샵에는 List와 Dictionary를 스레드로부터 안전하게 캡슐화시킨 ConcurrentBag, ConcurrentDictionary 키워드도 있다.

스레드로부터 안전한 컬렉션을 찾는다면 ConcurrentBag이나 ConcurrentDictionary를 사용하는것이 좋다.

불필요한 lock ConcurrentBag ConcurrentDictionary 의 사용은 성능저하의 원인이니(보통 lock에 대한 비용이 들어간다 라고 한다.)

lock이 필요한 자원or 로직인지, 멤버변수 자원을 꼭 공유해야 하는지, 확인해보고 사용하는게 좋다.

C# lock 사용법 및 예제

profile
나의 archive

0개의 댓글