2015년 3월 12일 목요일

뇌자극 C# 5.0 - 19 강

-쓰레드와 태스크-

19.1 프로세스와 스레드


프로세스 = 실행 파일이 실행되어 메모리에 적재된 인스턴스
스레드 = OS기 CPU 시간을 할당하는 기본 단위

프로세스가 밧줄이라면 스레드는 밧줄을 이루는 실이라고 할 수 있다.
프로세스는 최소한 한 개의 스레드를 가지고 있다

멀티 스레드의 장점과 단점


장점
(1) 사용자 대화형 프로그램에서 응답성을 높일 수 있다.
(2) 멀티 프로세스에 비해 멀티 스레드 방식이 자원 공유가 쉽다
(3) 이미 프로세스에 할당된 메모리와 자원을 그대로 사용해서 경제성이 좋다.

단점
(1) 구현하기가 매우 까다롭다.
(2) 테스트도 하기 힘들다
(3) 디버깅도 힘들다
(4) 자식 스레드 중 하나에 문제가 생기면 전체 프로세스가 문제가 생긴다.
(5) 너무 많이 사용하면 오히려 성능이 저하된다.

19.1.1 스레드 시작하기


(1)Thread 의 인스턴스를 생성한다. 생성자의 매개 변수로 스레드가 실행할 메소드를 매개 변수로 넘긴다

(2) Thread.start() 메소드를 호출하여 스레드를 시작한다

(3) Thread.Join() 메소드를 호출하여 스레드가 끝날 때까지 기다린다.

19.1.2 스레드 임의로 종료시키기


Thread 객체의 Abort() 메소드를 호출해주면 된다.
하지만 Abort() 메소드를 호출한다고 해서 동작하던 스레드가 즉시 종료된다는 보장이 없다.
예외가 있으면 예외처리까지 다 하고 나서야 종료가 되기 때문에 시간이 얼마나 걸릴지 모른다.
책에서는 Thread.Abort()를 사용하지 말라고 권고하고 있다(그럼 알려주질 말지 -_-)

19.1.3 스레드의 일생과 상태 변화


Unstarted - 스레드 객체를 생성한뒤 Start() 메소드가 호출되기 전 상태
Runing - 스레드가 시작하여 동작 중인 상태
Suspended - 스레드의 일시 중단 상태
WaitSleepJoin - 스레드가 블록(Block)된 상태
Aborted - 스레드가 취소된 상태
Stopped - 중지된 스레드의 상태
Background - 스레드가 백그라운드로 동작하고 있는 상태

Thread 객체의 ThreadState 필드를 통해 상태를 확인할 때는 반드시 비트 연산을 이용해야 한다.
ThreadState 열거형이 여러 상태를 동시에 나타낼 수 있도록 만들어져 있기 때문

19.1.4 스레드를 임의로 멈추는 또 하나의 방법 : 인터럽트


스레드를 강제로 종료시켜야 하는 경우에는
Thread.Interrupt()메소드를 사용하는 것이 안전하다

이유는 스레드의 상태가 WaitsleepJoin 상태면 바로 중지 시키지만
이 상태가 아닌 경우에는 스레드를 지켜보다가
WaitSleepJoin 상태가 되면 그때야 중단 시킨다

이런 특징 때문에 프로그래머는 최소한 코드가 "절대로 중단 되면 안 되는"
작업을 하고 있을 때 중단되지 않는 다는 보장을 받을 수 있다.

19.1.5 스레드 간의 동기화


동기화란? 스레드들이 순서를 갖춰 자원을 사용하게 하는 것을 "동기화"라고 한다.

스레드 동기화에서 가장 중요한 점은
"자원을 한번에 하나의 스레드가 사용하도록 보장"하는 것이다.

Lock 키워드로 동기화 하기 
예제)
class Counter
{
   public int count = 0;
   private readonly object thisLock = new object();

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

//...
MyClass obj = new MyClass();
Thread t1 = new Thread(new ThreadStart(obj.DoSomething);
Thread t2 = new Thread(new ThreadStart(obj.DoSomething);
Thread t3 = new Thread(new ThreadStart(obj.DoSomething);

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

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

lock 키워드의 매개 변수로 사용하는 객체는 참조형이면 어느 것이든 쓸 수 있지만
외부 코드에서도 접근할 수 있는 다음 세가지는 절대 사용해선 안된다.

this : 클래스의 인스턴스는 클래스의 내부뿐만 아니라 외부에서 자주 사용된다.
Type 형식 : typeof 연산자나 object 클래스로부터 물려받은 GetType() 메소드는 Type 형식의 인스턴스를 반환한다.
string 형식 : 외부에서 접근 가능하기에 절대로 써선 안된다.

Monitor 클래스로 동기화 하기

Monitor 클래스는 스레드 동기화에 사용하는 몇 가지 정적 메소드를 제공한다

Monitor.Enter()/Monitor.Exit() 를 사용한 예제

public void Increase()
{
   int loopCount = 1000;
   while(loopCount--> 0)
   {
      Monitor.Enter(thisLock);
      try
      {
         count++;
      }
      finally
      {
         Monitor.Exit(thisLock);
      }
   }
}

Monitor.Wait()와 Monitor.Pulse()로 하는 저수준 동기화 예제

이 두 메소드는 반드시 look 블록 안에서 호출해야 한다.

(1) 클래스 안에 다음과 같이 동기화 객체 필드를 선언한다.
readonly object thisLock = new object();

(2) 스레드를 WaitSleepJoin 상태로 바꿔 블록시킬 조건을 결정할 필드를 선언
bool lockedCount = false;

(3) 스레드를 블록시키고 싶은 곳에서는 lock 블록 안에서 2번에서 선언한 필드를 검사하여 Monitor.Wait()를 호출한다.

lock(thisLock)
{
   while(lockedCount == true)
       Monitor.Wait(thisLock);
   //....
}

(4) 3번 과정에서 선언한 코드는 count가 0보다 크거나 lockedCount 가 true면 해당 스레드는 블록된다. 이렇게 블록되어 있던 스레드를 깨워나면 작업을 해야 한다.
가장 먼저 2번 과정에서 선언한 lockedCount의 값을 true로 변경한다.
이렇게 해두면 다른 스레드가 이 코드에 접근할 때 3번 과정에서 선언해둔 블로킹 코드에 걸려 같은 코드를 실행할 수 없게 된다.
작업을 마치면 lockedCount 의 값을 다시 false로 바꾼 뒤 Monitor.Pulse()를 호출한다.
그럼 Waiting Queue에 대기하고 있던 다른 스레드가 깨어나서 false로 바뀐 lockedCount를 보고 작업을 수행한다.

lock(thisLock)
{
   while(lockedCount == true)
      Monitor.Wait(thisLock);
   lockedCount = true;
   count++;
   lockedCount = false;
 
   Monitor.Pulse(thisLock);
}

19.2 Task와 Task<TResult>, 그리고 Parallel


비동기 코드를 작성할 수 있도록 하는 도구와 장치

19.2.1 System.Threading.Tasks.Task 클래스


동기 코드 = 메소드를 호출한 뒤에 실행이 완전히 종료되어야만 다음 메소드를 호출 가능

비동기 코드 = 메소드를 호출한 뒤에 메소드의 종료를 기다리지 않고 바로 다음 코드를 실행하는 것.

Task 클래스는 인스턴스를 생성할 때 Action 델리게이트를 넘겨받는다.
반환형을 갖지 않는 메소드와 익명 메소드, 무명 함수 등을 넘겨받는다.

(예제)
Action someAction= () =>
{
   Thread.Sleep(1000);
   Console.WriteLine("Printed asynchronously.");
};

Task myTask = new Task(someAction);
myTask.Start(); //생성자에서 넘겨받은 무명 함수를 비동기로 호출한다.

Console.WriteLine("Printed synchronously.");

myTask.Wait(); //myTask 비동기 호출이 완료될 때까지 기다린다.

19.2.2 코드의 비동기 실행 결과를 주는 Task<TResult> 클래스


(예제)
var myTask = Task<List<int>>.Run(
   ()=>
   {
      Tread.Sleep(1000);
      List<int> list = new List<int>();
      list.Add(3);
      list.Add(4);
      list.Add(5);
      return list;
   }
);

var myList = new List<int>();
myList.Add(0);
myList.Add(1);
myList.Add(2);

myTask.Wait();
myList.AddRange(myTask.Result.ToArray());

19.2.3 손쉬운 병렬 처리를 가능케 하는 Parallel 클래스


void SomeMethod(int i)
{
   Console.WriteLins(i);
}
//...
Parallel.For(0, 100, SomeMethod);

위 코드는 SomeMethod()를 병렬로 호출하면서
0~100 사이의 정수를 메소드의 매개변수로 넘긴다.

SomeMethod() 메소드를 병렬로 호출할 때
몇 개의 스레드를 사용할지는 Parallel 클래스가 내부적으로 판단하여 최적화 한다.

19.3 async 한정자와 await 연산자로 만드는 비동기 코드


async 한정자는 메소드, 이벤트 처리기, 테스크, 람다식 등을 수식함으로서
C# 컴파일러가 이들을 호출하는 코드를 만날 때
호출 결과를 기다리지 않고 바로 다음 코드로 이동하도록 실행 코드를 생성하게 한다.

(1)async로 한정하 void 형식 메소드는 await 연산자가 없어도 비동기로 실행된다.
(2)async로 한정한 Task 또는 Task<TResult>를 반환하는 메소드/태스크/람다식은
   await 연산자를 만나는 곳에 호출자에게 제어를 돌려주며,
   await 연산자가 없는 경우 동기로 실행된다.

댓글 없음:

댓글 쓰기