Thread

ㅊㄷㅁ·2023년 10월 6일
0

StepByStepCS

목록 보기
2/2

Thread 란?


  • 프로세스가 실행될 때 내부에서 실질적으로 작업을 실행하는 주체이다.
  • 스레드는 하나의 프로세스 내에서 실행되는 단위로, 프로세스 내부의 코드와 데이터에 접근할 수 있다.
  • 프로세스 메모리 안에서 자원을 나눠 사용한다(코드, 데이터, 힙 영역은 공유, 스택은 비공유).
  • OS가 CPU 시간을 할당하는 기본 단위이다.

프로세스 ?
운영체제에 의해 관리되는 독립적인 실행 단위로 자체 메모리 공간을 가지며, 그로인해 프로세스 간 데이터 공유가 어렵습니다.
데이터 공유를 위해서는 별도의 메커니즘(파이프, 소켓, 파일 등)이 필요합니다.

즉, 하나의 Process가 실행되면 최소한 하나 이상의 Thread를 통해 작업을 하며 해당 Threads는 메모리를 공유 하며 다른 쓰레드의 내부의 코드와 데이터에 접근할 수 있다.

Multi Thread


Sync

  • Thread의 기본 작업 방식으로 작업이 순차적으로 실행되며 하나의 작업이 종료되면 기다리던 다른 작업이 실행된다.

  • 대부분의 프로그래밍 작업은 동기적으로 수행되며, 코드 흐름이 한 줄씩 진행된다.

Ex ) A,B,C,D
A -> B -> C -> D

Async

  • 작업을 수행 할 때 효율적인 처리를 위해 동기(Sync)가 아닌 여러 작업을 동시에 사용하는 비동기(Async)를 할 수 있다.

  • 비동기 작업을 사용하면 I/O 작업, 네트워크 호출 등과 같이 시간이 오래 걸리는 작업을 처리할 때 응용 프로그램의 반응성을 향상 할 수 있다.

  • 비동기 작업을 위한 스레드 관리와 동기화는보다 복잡할 수 있으므로 꼭 적절한 도구와 패턴(예: Task, async/await)을 사용하여 개발해야 한다.

  1. Task

    • .NetFramework에서 비동기적 작업을 나타내는 클래스이다.
    • 비동기 메서드에서 Task를 반환하면 작업을 시작하고 호출자에게 Task를 반환합니다.
  2. async

    • async는 비동기 작업을 수행하는 메서드를 나타낸다.
    • async 내부에서 await 키워드를 사용해 다른 비동기 메서드를 호출한다.
  3. await

    • await는 비동기 작업의 완료를 기다리는 데 사용됩니다. await가 있는 코드 블록에서 비동기 작업이 완료될 때까지 스레드를 차단하지 않고 대기

Ex ) A,B,C,D
A -> C
B -> D

Threads Synchronization

앞서 말한 바와 같이 Async를 사용할 때 동기화와 코드 관리에 주의를 더욱 기울여야 한다. 예를 들어 2개의 스레드가 1개의 count = 0 변수를 증가시킬 때에 동시에 count를 가져가 사용하면 두 번 증가했지만 각각의 작업은 해당 변수가 0 인 상태에서 실행되었기 때문에 문제가 발생할 여지가 있다.

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

크리티컬 섹션(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하는 기능을 가지고 있다.

C#

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        Console.WriteLine("Start");

        // 비동기 작업을 수행하는 메서드 호출
        await SomeAsyncMethod();

        Console.WriteLine("End");
    }

    static async Task SomeAsyncMethod()
    {
        Console.WriteLine("Async Method Start");

        // 비동기적으로 대기
        await Task.Delay(2000); // 2초 동안 대기

        Console.WriteLine("Async Method End");
    }
}

Java

import java.util.concurrent.*;

public class AsyncExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executorService = Executors.newFixedThreadPool(2); // 스레드 풀 생성

        // 비동기 작업 실행 및 Future 객체 얻기
        Future<Integer> future = executorService.submit(() -> {
            Thread.sleep(2000); // 2초 동안 대기
            return 42;
        });

        System.out.println("작업 시작");

        // 작업이 완료될 때까지 대기하고 결과 가져오기
        int result = future.get();

        System.out.println("작업 결과: " + result);

        // ExecutorService 종료
        executorService.shutdown();
    }
}

예제

// 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);
            }
        }
    }
}

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

참고
쓰레드 복습을 위해 작성하는 글

profile
ㅊㄷㅁ

0개의 댓글