C# 멀티 쓰레드

문승현·2022년 11월 5일
0

BeDev_5

목록 보기
2/3
post-thumbnail

운영체제는 여러 프로세스를 동시에 실행할 수 있는 능력을 갖추고 있다.
유튜브로 음악을 들으면서 비주얼 스튜디어로 코딩을할 수 있다.
운영체제만 동시에 여러 작업을 할 수 있는 것은 아니다.
프로세스도 한번에 여러 작업을 할 수 있다.
워드프로세서에서 글을 써 내려가는 작업과 함께 문법을 검사하는 작업이 동시에 가능하다.

프로세스는 실행 파일이 실행되어 메모리에 적재된 인스턴스이다.
word.exe가 실행 파일이라면
실행 파일에 담겨 있는 데이터와 코드가 메모리에 적재되어 동작하는 것이 프로세스다.
프로세스는 반드시 하나 이상의 스레드로 구성된다.
스레드는 운영체제가 CPU 시간을 할당하는 기본 단위이다.

C#은 여러 개의 스레드를 가지는 멀티 스레드 구조의 프로그램을 지원한다.
멀티 스레드의 장점은 응답성을 높일 수 있고, 자원 공유가 쉽고, 경제적이다.
단점은 구현이 복잡하고, 안정성을 약화시킬 수 있고, 과용하면 성능이 저하될수 있다.

C#은 스레드를 제어하는 클래스로 System.Threading.Thread를 제공한다.
예를 들어 아래와 같이 Thread의 인스턴스를 생성한다고 하자.
이때 생성자의 인수로 스레드가 실행할 메소드를 넘길 수 있다.
Thread.Start()메소드를 호출하여 생성한 스레드를 시작한다.
Thread.Join() 메소드를 호출하여 해당 스레드가 끝날 때까지 기다린다.

static void DoSomething()
{
  for (var i = 0; i < 5; i++)
  {
    Console.WriteLine($"Do Something : {i});
  }
}

static void Main(string[] args)
{
  // 스레드 인스턴스 생성
  Thread thread1 = new Thread(new ThreadStart(DoSomething));

  Console.WriteLine("Start")
  // 스레드 시작
  thread1.Start();
  
  // 스레드 종료 대기
  thread1.Join();
  
  Console.WriteLine("End")
}

스레드가 메모리에 적재되는 시점은 thread1.Start() 메소드를 호출했을 때이다.
thread1.Start() 메소드가 호출되고 나면,
CLR은 스레드를 실제로 생성하여 DoSomething() 메소드를 호출한다.
thread1.Join() 메소드는 블록되어 있다가 DoSomething() 메소드의 실행이 끝나면,
즉 thread1의 실행이 끝나면 반환되어 다음 코드를 실행할 수 있게 한다.

using System;
using System.Threading;

namespace BasicThread
{
  class MainApp
  {
    static void DoSomething()
    {
      	for (var i = 0; i < 5; i++)
        {
          Console.WriteLine($"DoSomething : {i}");
          Thread.Sleep(10);
        }
    }
    
    static void Main(string[] args)
    {
      Thread thread1 = new Thread(new ThreadStart(DoSomething));
      
      Console.WriteLine("Start thread...");
      
      thread1.Start();
   	
      // thread1의 DoSomething() 메소드가 실행되는 동시에 메인 스레드의 이 반복문도 실행된다.
      for (var i = 0; i < 5; i ++)
      {
        Console.WriteLine($"Main : {i}");
        Thread.Sleep(10);
      }
      
      Console.WriteLine("Wating untill thread stops...");
      thread1.Join();
      
      Console.WriteLine("Fininshed");
    }
  }
}

스레드는 파일 핸들이나 네트워크 커넥션, 메모리에 선언한 변수 등
여러 가지 자원을 다른 스레드들과 공유하는 경우가 많다.
그래서 어떤 스레드가 특정 자원을 사용하고 있는 중인데 갑자기 다른 스레드가 끼어들어
해당 자원을 사용해버리면 프로그램에 문제가 발생할 수 있다.

스레드들이 순서를 갖춰 자원을 사용하게 하는 것을 일컬어 동기화라고 한다.
스레듣 동기화에서 중요한 것은 자원을 한 번에 하나의 스레드가 사용하도록 보장하는 것이다.
.NET이 제공하는 대표적인 도구로 lock 키워드와 Monitor 클래스가 있다.
lock 키워드로 감싸주면 해당 코드는 크리티컬 섹션으로 바뀐다.

class Counter
{
  public int count = 0;
  public void Increase()
  {
    count += 1;
  }
}

var obj = new Counter();

var t1 = new Thread(new ThreadStart(obj.Increase()));
var t2 = new Thread(new ThreadStart(obj.Increase()));
var t3 = new Thread(new ThreadStart(obj.Increase()));

t1.Start();
t2.Start();
t3.Start();

t1.Join();
t2.Join();
t3.Join();

Console.WriteLine(obj.count);

결과 값은 3일 수도 있고 아닐 수도 있다.
한 스레드가 실행하고 있을 때 다른 스레드는 실행하지 못하도록 하는 장치가 필요하다.

class Counter
{
  public int count = 0;
  public readonly object thisLock = new object();

  public void Increase()
  {
    lock (thisLock)
    {
          count += 1;
    }
  }
}

var obj = new Counter();

var t1 = new Thread(new ThreadStart(obj.Increase()));
var t2 = new Thread(new ThreadStart(obj.Increase()));
var t3 = new Thread(new ThreadStart(obj.Increase()));

t1.Start();
t2.Start();
t3.Start();

t1.Join();
t2.Join();
t3.Join();

Console.WriteLine(obj.count);

lock을 너무 무분별하게 사용하면 프로그램의 성능이 크게 떨어진다.

Monitor 클래스는 스레드 동기화에 사용하는 몇 가지 정적 메소드를 제공한다.
Monitor.Enter()와 Monitor.Exit() 메소드는 lock 키워드와 동일한 기능을 제공한다.
Monitor.Enter() 메소드가 크리티컬 섹션을 만들고 Monitor.Exit() 메소드가 크리티컬 섹션을 제거한다.
사실 lock 키워드가 Monitor 클래스의 Enter(), Exit() 메소드 바탕으로 구현되어 있다.

Monitor.Wait(), Monitor.Pulse()
조금 더 섬세한 스레드간의 동기화를 가능하게 해주는 메소드
lock 블록 안에서 호출해야 한다.

Wait() 메소드는 스레들 WaitSleepJoin 상태로 만든다.
해당 상태에 들어간 스레드는 동기화를 위해 갖고 있던 lock을 내려 놓은 뒤
Wating Queue라고 하는 큐에 입력되고, 다른 스레드가 락을 얻어 작업을 수행한다.
작업을 수행하던 스레드가 일을 마친 뒤 Pulse() 메소드를 호출하면
CLR은 Waiting Queue에서 스레드를 꺼내 Ready Queue에 입력시킨다.
Ready Queue에 있는 스레드는 차례에 따라 lock을 얻어 Running 상태에 들어간다.
Monitor.Wait() 메소드는 Monitor.Pulse() 메소드가 호출되면 바로 깨어난다.
멀티 스레드 어플리케이션의 성능 향상을 위해서 Monitor.Wait()와 Monitor.Pulse() 를 사용한다.

using Systme;
using System.Threading;

namespace WaitPulse
{
  class Counter
  {
    const int LOOP_COUNT = 1000;
  	readonly object thisLock;
  	bool lockedCount = false;
  
  	private int count;
	
  	public int Count
    {
      get { return count; }
    }
  	
  	public Counter()
  	{
      thisLock = new object();
      count = 0;
    }
  	
  	public void Increase()
  	{
      int loopCount = LOOP_COUNT;
      
      while (loopCount-- > 0)
      {
        lock (thisLock)
        {
          while (count > 0 || lockedCount == true)
          {
			Monitor.Wait(thisLock);
          }
          
          lockedCount = true;
          count++;
          lockedCount = false;
          
          Monitor.Pulse();
        }
      }
    }
  
  	public void Decrease()
  	{
      int loopCount = LOOP_COUNT;
      
      while (loopCount-- > 0)
      {
        lock (thisLock)
        {
          while (count < 0 || lockedCount == true)
          {
			Monitor.Wait(thisLock);
          }
          
          lockedCount = true;
          count--;
          lockedCount = false;
          
          Monitor.Pulse();
        }
      }
    }
  }
  class MainApp
  {
    static void Main(string[] args)
    {
      Counter counter = new Counter();
      Thread incThread = new Thread(new ThreadStart(counter.Increase));
      Thread decThread = new Thread(new ThreadStart(counter.Decrease));
      
      incThread.Start();
      decThread.Start();
      incThread.Join();
      decThread.Join();
      
      Console.WriteLine(counter.Count);
    }
  }
}

0개의 댓글