C# - 쓰레드) 복습을 위해 작성하는 글 2023-04-23

rizz·2023년 4월 23일
0

C

목록 보기
10/25

📒 갈무리 - 스레드(thread)

📌 스레드(thread)란?

- OS가 CPU 시간을 할당하는 기본 단위

- 하나 이상의 스레드로 구성

- 프로세스 내부에서 생성되는 실제로 작업을 하는 주체

- 프로세스 안에 있는 스레드들은 코드, 데이터, 힙 영역은 공유하고 스택은 각각 따로 할당된다.

📌 프로세스(process)란?

- 실행 중인 프로그램(메모리에 적재된 프로그램)

- 스케줄링: 생성 > 준비 > 실행 > 대기 ... > 종료

- 하나의 프로세스는 여러 개의 스레드를 가질 수 있다.

 

📌 스레드의 장점

- 스레드를 사용하면 이미 프로세스에 할당된 메모리와 자원을 그대로 사용하기 때문에 메모리를 절약할 수 있다.

- 프로그램이 일을 하고 있을 때, 대기할 필요가 없이 다른 일을 처리할 수 있다.

- 프로세스끼리 데이터를 공유할 때, IPC(Inter Process Communication)을 이용해야 하지만, 스레드는 코드 내의 데이터들을 같이 사용할 수 있다.

📌 스레드의 단점

소프트웨어 구현 난이도가 높다.

디버깅 난이도 또한 높다.

하나의 스레드에 문제가 발생하면 전체 프로세스에 영향을 미칠 가능성이 높다.

스레드가 CPU를 사용하기 위해서는 작업 간 전환(Context Switching)을 해야 한다.

 

📌 동시성 프로세스

- 여러 개의 프로세스가 있을 때, 하나의 프로세스마다 일부분씩 작업을 진행하며 진행 중인 작업을 바꾸는 방식(Context Switching)

이 일이 매우 빠르게 진행되기 때문에 사용자는 마치 동시에 진행되는 것처럼 느끼게 된다.

📌 병렬성 프로세스

- 프로세스 하나에 코어 여러 개가 달려서 각각 동시에 작업을 수행하는 방식(듀얼 코어, 쿼드 코어, 옥타 코어 등 멀티 코어 프로세스가 있는 컴퓨터에서 가능한 방식)

 

📌 ThreadState

Aborted : 스레드 상태에 AbortRequested가 포함되어 있고 스레드가 작동하지 않지만 상태가 아직 Stopped로 변경되지 않은 상태

AbortRequested : 스레드에 Abort(Object) 메서드가 호출되었지만 해당 스레드는 자신을 종결시키려는 보류된 ThreadAbortException을 받지 못한 상태

Background : 스레드가 백그라운드로 동작되고 있는 상태. Foreground 스레드는 하나라도 실행되고 있는 한 프로세스가 종료되지 않지만, Background는 여러 개가 실행 중이어도 프로세스 상태에 영향을 미치지 않는다. 하지만 프로세스가 종료되면 Background 스레드 또한 모두 종료된다. Thread.IsBackground 속성에 true 값을 입력하면 스레드를 이 상태로 바꿀 수 있다.

Running : 스레드가 시작하여 동작 중인 상태(Unstarted 상태일 때, Thread.Start() 메소드를 통해 Running 상태로 만들 수 있다.)

Stopped : 스레드가 중지된 상태

SuspendRequested : 스레드를 일시 중단하도록 요청하는 상태

Unstarted : 스레드 객체를 생성한 후 Thread.Start()메소드가 호출되기 전의 상태

WaitSleepJoin : 스레드가 차단된 상태(Sleep(), Join(), Enter(), Wait() 등의 함수로 잠금을 허용하거나, ManualResetEvent와 같은 스레드 동기화 개체에서 대기 중인 경우)

 

📌 스레드 중단하기

Abort

- 함수의 종료를 보장하지 못해, 권장하지 않음

- .NET 5 이상 버전에서는 이 메서드가 사용되지 않고 호출하면 컴파일 시간 경고가 생성됨

interrupt

- 스레드가 동작 중인 상태(Running 상태)를 피해서 WaitJoinSleep 상태에 들어갔을 때 ThreadInterruptedException 예외를 던짐으로 인해 스레드를 중지 시킨다.

- 스레드가 이미 WaitJoinSleep 상태라면 즉시 중단이 가능하지만, 이 상태가 아니라면 스레드를 지켜보고 있다가 WaitJoinSleep 상태가 되었을 때 스레드를 중단 시킨다. 그러므로 WaitJoinSleep 상태가 될 일이 없는 작업을 하고 있을 때에는 중단되지 않는다는 보장을 받을 수 있다.

Join

- 현재 스레드에서 다른 스레드의 실행이 끝나길 기다릴 때 사용

- Join을 활용하여 동기화 작업을 수행할 수 있다.

- 특정 스레드의 작업이 종료되었는지 확인할 수 있다.

 

📌 스레드 동기화

- 데이터나 변수들을 공유하는 경우

- 여러 개의 스레드가 데이터나 변수들을 공유하게 되면 문제가 발생하게 된다.

예를 들어, 스레드1과 스레드2가 "count라는 변수의 값을 증가시킨다."라고 가정했을 때, count가 0일 때 서로 동시에 가져다가 증가시키게 되면 count를 두 번 증가시킨 것이지만, 실제로는 두 스레드 모두 count의 값이 0일 때 가져갔기 때문에 결국에는 count의 값이 1이 되게 되는 것이다.

이 때문에 스레드들이 질서 있게 자원을 사용할 수 있도록 할 필요가 있다.

크리티컬 섹션(Critical Section)

- 한 번에 한 스레드만 사용할 수 있는 코드 영역

- 스레드의 동기화를 설계할 때는 크리티컬 섹션이 반드시 필요한 곳에만 사용하도록 하는 것이 중요하다. 그렇지 않으면 큰 성능의 하락을 초래하게 된다.

배타적 잠금(Exclusive Lock)

- 공유 데이터를 오직 하나의 스레드만 접근할 수 있다.(lock 키워드, Monitor, Mutex, SpinLock 등을 사용하여 구현)

비배타적 잠금(Non-Exclusive Lock)

- 공유 데이터에 접근할 수 있는 스레드의 개수를 제한할 수 있다.(Semaphore, SemphoreSlim, ReaderWriterLockSlim 등을 사용하여 구현)

lock

- lock 키워드를 사용하여 감싸주면 해당 코드를 크리티컬 섹션으로 만들 수 있다.

- 외부 코드에서도 접근할 수 있는 것은 매개 변수로 사용하지 않는 것이 좋다.(this, string 객체, Type 형식 등)

Monitor

- 스레드 동기화에 사용되는 메소드를 제공

- 하나의 프로세스 안에서만 사용이 가능

Mutex

- Monitor 클래스와 유사한 기능을 제공

- Monitor 클래스는 하나의 프로세스에서만 사용이 가능하지만, Mutex 클래스는 서로 다른 프로세스 간에서도 배타적으로 Locking하는 기능을 가지고 있다.

 

📌 스레드 예제

스레드 4개를 이용하여 라면을 조리하는 예제

본 예제는 https://www.youtube.com/watch?v=iks_Xb9DtTM 를 참고하여 C#으로 재구현한 것입니다.

// C#
using System;
using System.Threading;

namespace Thread_EX
{
    class RamenCook
    {
        private int ramenCount;
        private string[] burners = { "_", "_", "_", "_" };
        public RamenCook(int count)
        {
            ramenCount = count;
        }
        public void Run() // Thread Run
        {
            while (ramenCount > 0)
            {
                // 다중의 스레드가 동시에 실행되면 ramenCount가 하나만 줄어드는 것을 방지하기
                // 위해서 lock으로 보호.
                lock (this)
                {
                    ramenCount--;
                    Console.WriteLine(Thread.CurrentThread.Name + " : " + ramenCount + "개 남음");
                }
                // 빈 버너를 찾고 해당 스레드의 이름으로 버너를 차지하고 라면 조리
                for (int i = 0; i < burners.Length; i++)
                {
                    if (!burners[i].Equals("_")) continue;

                    // 다중의 스레드가 동시에 불을 켜지 못하도록 방지
                    lock (this)
                    {
                        burners[i] = Thread.CurrentThread.Name;
                        Console.WriteLine("                      " + Thread.CurrentThread.Name + " : [" + (i + 1) + "]번 버너 ON");
                        ShowBurners();
                    }

                    try
                    {
                        // 해당 스레드를 일정시간 정지(라면이 끓을 시간을 표현해 보았음...)
                        Thread.Sleep(2000); // 2초
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine("Stack Trace1 : " + e.StackTrace);
                    }

                    // 해당 버너를 끄고 비움
                    lock (this)
                    {
                        burners[i] = "_";
                        Console.WriteLine("                              " + Thread.CurrentThread.Name + " : [" + (i + 1) + "]번 버너 OFF");
                        ShowBurners();
                    }
                    break;
                }
                try
                {
                    // 어느정도 순서가 뒤엉키도록
                    // 스레드마다 다음 라면을 끓이기까지 랜덤의 시간을 부여.
                    Random random = new Random();
                    Thread.Sleep(1000 * random.Next(1, 3));
                }
                catch (Exception e)
                {
                    Console.WriteLine("Stack Trace2 : " + e.StackTrace);
                }
            } // end while
        }

        private void ShowBurners()
        {
            String stringToPrint = "                                                            ";

            for (int i = 0; i < burners.Length; i++)
            {
                stringToPrint += (" " + burners[i]);
            }
            Console.WriteLine(stringToPrint);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                Console.WriteLine("만들 라면의 개수를 입력");
                int ramenCount = int.Parse(Console.ReadLine());
                RamenCook ramenCook = new RamenCook(ramenCount);

                // 스레드 생성
                Thread t1 = new Thread(ramenCook.Run);
                Thread t2 = new Thread(ramenCook.Run);
                Thread t3 = new Thread(ramenCook.Run);
                Thread t4 = new Thread(ramenCook.Run);

                // 스레드의 이름을 설정
                t1.Name = "A";
                t2.Name = "B";
                t3.Name = "C";
                t4.Name = "D";

                // 스레드 시작
                t1.Start();
                t2.Start();
                t3.Start();
                t4.Start();
            }
            catch (Exception e)
            {
                Console.WriteLine("Exception Message" + e.Message);
            }
        }
    }
}

// 위 코드를 실행해 보면, 순서가 섞여 나오는 것을 볼 수 있다.
// 각각이 동시에 실행이 된다는 것을 확인할 수 있다.

자바에서는 Runnable interface를 상속받아 Run 메소드를 구현하여 스레드를 동작 시키지만, .NET은 Runnable interface를 지원하지 않기 때문에 delegate를 Thread에 매개 변수에 전달해 줌으로써, Thread를 동작시킬 수 있다.

profile
복습하기 위해 쓰는 글

0개의 댓글