1. Thread 탄생의 배경:

초창기 컴퓨터는 단일스레드로 하나만 가지다보니 수행시간이 오래걸리는 작업을 하게되면 동시에 다른작업을 할수 없었다. 버그가 발생시 무한루프에 빠져 컴퓨터가 멈춰버리는 현상이 벌여졌다. 그럴때 파워버튼을 눌러 재시작하는 방법밖에없었고 가장큰 단점은 응용프로그램들이 메모리상에 가지고 있던 정보를 모두 날려버릴 수밖에 없었다.

Ms 응용프로그램들이 "프로세스" 라는 공간에 저장되게 설계하였고 단일 인스턴스가 사용하는 리소스의 집합체이며, 개별 프로세스는 자신만의 가상 주소 공간을 가지고 있어서 다른 프로세스가 자신의 코드나 데이터에 접근할 없도록 하였다.

Cpu입장에서는 만약 프로세스가 여러개이고 cpu 1개일때 데이터가 손상될 여지가 없고 좀더안전할지는 몰라도 무한루프를 실행하느라 다른코드를 수행할수 없거나 요청에 응답할수 없는 상태가 될수있다.

Ms 이러한 문제를 해결해야 했고, 그결과로 만들어 것이 "Thread". 스레드는 cpu ㅏ상화하기 위한 윈도우 운영체제의 개념이다.

윈도우 운영체제는 개별 프로세스에게 각자의 스레드(마치 cpu 유사한 기능을 수행하는)

  1. Thread 비용:

스레드는 추가적인(메모리소비) 시간(수행 시간 성능) 비용을 발생시킨다.

  • 스레드 커널객체

Os system 내에 생성되는 개별 스레드별로고유의 데이터 구조체를 할당하고 초기화한다.

데이터 구조체는 스레드를 나타내는 여러 속성들을 가지고 있으며, 스레드의 컨텍스트라고 불리는 정보도 포함되어있다. 컨텍스트는 cpu 레지스터들의 값을 저장하고 있는 메모리 블록이며, x86, x64, ARM 아키텍쳐로 대략 700, 1240, 350byte 메모리 공간이 필요하다.

  • 스레드 환경블록(Thread environment block, TEB)

TEB 유저 모드(응용프로그램이 빠르게 접근할수 있는 주소공간) 할당되고 초기화도는 메모리 블록이다. TEB1페이지크기(x86, x64, ARM CPU에서 4KB) 메모리를 사용한다. TEB 스레드의 예외 처리 체인 앞쪽에 새로운 노드를 추가하고, 스레드가 try 블록을 빠져나오면 추가하였던 노드를 제거한다.

추가적으로 TEB 스레드의 스레드 로컬저장소(Thread local storage, TLS) 데이터와 GDI OpenGL 그래픽과 관련된 데이터 구조체를 가지고 있기도 하다.

  • 유저 모드 스택

지역변수와 매개변수를 저장할 용도로 사용된다. 현재 수행중인 함수가 반환될떄 그다음으로 수행해야 위치를 저장하고 있다.( callback 함수 개념 ), 기본적으로 윈도우 운영체제는 각각의 스레드에 대해서 유저 모드 스택으로 1MB 할당한다. 1MB 주소공간을 reserve 해두고 스레드가 실제로 스택으로 더많은 공간이 필요로 하는 경우에 한해서 조금씩 나누어 물리적 저장소에 커밋한다.

  • 커널 모드 스택

응용프로그램이 운영체제의 커널 모드 함수로 매개변수를 전달해야할때 사용한다. 보안상의 이유로 유저모드 코드에서 커널모드 매개변수를 전달할 유저모드 스택의 내용을 커널모드 스택으로 복사해 넘겨준다. 복사가 완료되면 커널은 매개변수의 값이 올바른지 검증하게된다. 응용프로그램은 커널모드 스택에 접근할수 없다. 매개변수의 값을 변경할수 없으므로 검증이 이루어진 이후에 커널 코드는 안전하게 매개변수를 처리할수 있게 된다.수행중인 함수가 반환될 다음으로 수행해야 위치를 저장하기도 한다. 커널모드 스택은 32bit 윈도우 에서는 12kb, 64 24kb 이다.

  • DLL 스레드 attach/detach 통지

Window os 프로세스 내에서 새로운 스레드가 생성되면 해당 프로세스 메모리 공간에 로드된 모든 비관리 DLL들의 DLLMain함수를 dll_thread_attach 플래그를 매개변수로 호출하게 된다. 몇몇 DLL들은 이러한 통지 규칙을 이용하여 스레드 별로 특수한 초기화 작업을 수행하거나 정리 할수 있다. 예를들어 C런타임 라이브러리는 DLL 경우 이러한 통지가 있을 스레드 로컬장소(TLS) 몇몇 상태 정보를 저장해두고, 해당 스레드가 C런타임 라이브럴 내의 특정 함수들을 사용할때 활용하곤한다.

  1. 컨텍스트 스위칭

운영체제는하나의 CPU 하나의 스레드를 할당한다. 스레드는 주어진시간 타임-슬라이스(퀀텀) 이라는 시간 동안만 수행되는데(30milli second 0.03), 메모리,성능 개선없이 순전히 수행하는 비용, 수행을 완료하고 나면 윈도우 운영체제는 다른 스레드로 컨텍스트 전환 한다. 컨텍스트 전환하려면 운영체제는 다음과 같은 작업을 수행한다.

  1. CPU 레지스터의 값을 현재 수행중인 스레드의 컨텍스트 구조체에 저장한다. 컨텍스트 구조체는 스레드 커널 객체 내부에 있다.
  2. 여러 스레드들 다음 번에 수행할 스레드를 선택한다. 만일 선택된 스레드가 다른 프로세스에 속해 있다면, 윈도우 운영체제는 스레드가 수행할 코드와 데이터에 접근하기 위해서 가상 메모리 주소를 먼저 전환해야한다.
  3. 선택된 스레드의 컨텍스트 구조체 내의 값을 CPU 레지스터로 로드한다.

특정 응용프로그램의 스레드가 무한루프에 빠졌다고 가정 해보자. 윈도우는 주기적으로 세레드의 수행을 가로채서 다른 스레드에게 cpu 할당할 있기 떄문에 다른 스레드가 일정시간동안 수행하게된다.

새롭게cpu 할당받은 스레드는 작업관리자와 같은 응용프로그램의 스레드일수도 있고 사용자는 작업관리자를 이용하여 무한루프에 빠진 프로세스를 종료되면서 프로세스를 강제 종료하게 되면 무한루프에 빠진 프로세스가 종료되면서 이프로세스가 사용하던 데이터도 모두 사라지게 된다. 다른프로세스는 아무런 문제없이 수행될 있다.

CPU캐시에 수행할 코드와 데이터를 이미 가지고 있기 때문에 상당한 대기 시간을 요하는 RAM 같은 메모리에 가능한 접근할 수있다. 하지만 win os 새로운 운영체제에 전환해 버리면 cpu캐시의 내용은 쓸모없는 데이터가 되어버린다. Cpu 적정 수준의 수행속도를 보장하기 위해 다시 cpu 캐시로 데이터를 읽어오게 되는데, 이과정에서 RAM 접근하지 않을 없다. 하지만 이또한 영원하지 않아서 30milli second 이후에 또다시 다른 스레드로 컨텍스트 전환을 수행하게 된다.

컨텍스트 전환 시에 소요되는 시간은 cpu 아키텍쳐와 수행속도에 따라 상당히 다양하다. Cpu캐시를 구성하는 시간 또한 어떠한 응용프로그램을 사용중인지, cpu캐시 크기는 얼마인지 요소에 결정된다. 이러한 이유로 컨텏트 전환 시마다 어느정도의 시간 비용이 발생하는지를 설명하거나 예측하는것은 사실상 불가능하다. 고성능 프로그램  개발시 관련된 비용이 발생할수 있음을 염두해 두고 회피하도록 개발해야한다.

*타임 슬라이스 만큼 특정 스레드를 수행한 이후에 동일 스레드를 다시 스케줄하게 되는 경우 (context switching 없이) 윈도우 운영체제는 컨텍스트 전환을 수행할 필요없이 단순 작업을 이어서 수행하게된다. 컨텍스트 전환이 일어나지 않게 코드 설계하는게 좋다.

*스레드가 자신에게 할당된 타임슬라이스 만큼 작업을 수행하지 않고 자발적으로 은시간을 반납하는경우가 종종 발생할 있다. 스레드는 종종 I/O 작업(키보드,마우스, 파일, 네트워크 등등) 완료할때까지 대기해야 하는 상황이 발생할 있는데, 예를들어 메모장의 스레드의 경우 사용자의 입력을 기다리는 동안 아무런 작업도 수행하지 않고 있다가 사용자가 키보드에서  J 입력하면 J키가 눌려진 것에 대한 작업을 수행하게된다. 이에 대한 처리가 5밀리초만에 끝났다면, 이작업을 수행 중이던 스레드는 win32 특정 함수를 호출하여 다음 입력 이벤트를 처리할 준비가 되었음을 운영체제에게 알려준다. 이상 처리해야 입력 이벤트가 없다면 윈도우 운영체제는 스레드를 대기상태로 변경하여(남은 타임-슬라이스는 자발적으로 포기하고) 다음번 입력이 있을떄까지 이스레드가 이상 처리해야 입력 이베느가 없다면 윈도우 운영체제는 대기상태로 변경하여(남은 타임슬라이스를 포기하고) 다음 입력이 있을때까지 이스레드가 이상 cpu시간을 할당 받지 않도록 한다.

추가적으로 가비지 수집 진행하려면 CLR 우선적으로 모든 스레드를 일시 정지시켜야한다. 관리 내의 객체를 마크하기 위해서 모든 스레드의 스택을 뒤져서 루트를 찾아야하고, 컴팩트 작업이 완료되면 스택 내의 루트를 갱신한 후에서야(컴팩트 작업을 수행하면 루트가 가리키는 객체의 위치가 변경되므로) 비로소 스레드를 재개할 있다.  따라서 가능한 여러개의 스레드를 사용하지 않는 편이 가비지 수집 단계의 성능을 향상시킬수 있다. 디버깅하다 보면 모든 스레드가 일시 정지되고, 한단께를 수행하면 모든 스레드가 수행을 제개했다가 다시 일시정지되는것을 알수있다. 스레드 수가 많아질 수록 디버깅 속도는 그에 비례해 느려지게된다. 최적의 성능은 스레드의 개수를 cpu 수와 일치 시키면 된다.

이유는 cpu 개수보다 ㅡ레드의 수가 많아지면 스레드간의 컨텍스트 스위칭이 필요해져서 성능에 좋지 않은 영향을 미치기 떄문이다.

  1. cpu 트렌드
  • 여러개의 cpu사용
  • 하이퍼스레드

인텔이 소유하고있 기술을 이용하면 단일의 칩이 마치 두개의 칩을 가지고 있는것 처럼 동작한다. Cpu 레지스터와 같이 구조적인 상태를 저장하기 위한 공간을 두세트 가지 고있기는 하지만두개의 cpu 설치한 것처럼 보이고 ㅏ라서 개의 스레드를 동시에 스케줄링한다. 하지만 cpu 여전히 특정시간에 그중 한개의 스레드만을 수행한다, 수행할 스레드가 캐시미스, 분기예측실패, 데이터 의존성 등으로 인해 잠시 멈추어야할 상황이 되면 다른 스레드를 수행하게된다. 하드웨어 수준에서 일어나게 되며, 운영체제는 cpu 내부적으로 어떤일이 일어나는지 전혀 알지 못하고, 단순히 두개의 스레드가 동시에 수행되고 있다고만 알게된다. Win os 하이퍼스레드 cpu 대해서 알고, 컴퓨터에 하이퍼스레드 cpu 여러 장착되어 있다면, 여러 스레드들을 동시에 수행하기 위해서 cpu별로 하나씩 스레드를 우선적으로 스케줄하고, 나머지 스레드들을 이미 수행중인 cpu 다시 할당하는것이 가능하다.

  • 멀티 코어
  1. CLR 스레드와 윈도우 스레드
  2. 계산 심의 비동기 작업을 수행하기 위한 전용 스레드
  3. 여러 스레드를 사용하는 이유
  • 응답성(클라이언트  GUI 응용프로그램에 대해서): 클라이언트 GUI 응용프로그램의 경우 사용자의 입력 이벤트에 대해서 계속해서 응답할 있어야 하기 때문에 수행해야하는 작업을 다른 작업 스레드를
  • 성능(클라, 서버 응용프로그램 모두에 대하여): 윈도우는 CPU별로 하나의 스레드만을 스케줄링하지만 여러 CPU 각자에게 할당된 스레드를 동시에 수행할 있기 떄문에 특정 시점에 병렬로 여러 작업을 동시에 수행할수 있고 이를 통해 성능개선 있다. 여러개의 CPU 가진 컴퓨터에서 수행되는 경우 한해서만 성능개선 효과가 나타난다.
  1. 스레드 스케줄링과 우선순위

선점형 운영체제는 어떤 스레드를 언제 얼마간 스케줄링할지 알고리즘이 필요하다.

스레드들은 커널객체 부에 컨텍스트 구조체를 포함하고있다. 구조체에는 스레드가 마지막으로 수행되었던 CPU레지스터의 상태 정보를 가지고 있다. OS 타임슬라이스 만큼 특정 스레드를 수해하고 나면 현존하는 모든 커널 객체중 다른 적이 완료되기를 기다리지 않는 스케줄 가능 스레드가 있는지 확인하게 되고, 이중 하나를 선택해서 컨텍스트 전환을 수행한다. 특정 스레드만을 항상 실행중인 상태로 유지하여 다른 스레드가 수행되지 않도록 수는 없다는 것이다.

윈도우 운영체제를 다양한 드라이브, 다양한 네트워크 등에서 폭넓게 수행되는 것이었기 떄문에, 운영체제를 실시간 OS 설계하지 않았따. CLR 관리코드를 수행하는 방식도 실시간과는 사뭇 거리가 있다. 실제로 수행중 DLL로딩, 코드에 대한 JIT컴파일, 가비지 수집등의 작업이 언제 수행될지는 예측하기 어렵다.

윈도우 토어앱은 추가적으로 앱도메인을 생성할 없고 프로세스의 우선순위 클래스를 변경할 수도 없을 뿐더러 스레드의 우선순위도 변경 없다. 뿐만 아니라  윈도우 스토어 앱의 경우 전면에 나타나지 않을 경우, 운영체제가 자동적으로 모든 스레드를 일시 정지시킨다. 이러한목적은 1) 백그라운드로 수행되는 앱이 전면에서 사용자와 상호작용하는 앱의 CPU 시간을 뻇지 못하여 빠르게 반응함 위함이고

  1. CPU 사용량을 감소시켜 배터리 전원을 사용하지 않도록 하여 충전없이 PC 오랫동안 쓰게하기위함이다.
  1. 포그라운드 스레드와 백그라운드 스레드

CLR 모든 스레드를 포그라운드 또는 백그라운드로 인식한다. 포그라운드 스레드가 종료되면 수행중인 백그라운드 스레드들을 모두 강제종료 하려고 시도하게되고, 백그라운드 스레드는 아무런 예외를 발생시키지 않고 즉각종료된다.

CLR 포그라운드 스레드와 벡그라운드 스레드의  개념을 앱도메인이 더욱 수행될수 있도록 하기 위해서 활용할 필요가 있다.

  1. 계산 중심의 비동기 작업

코드 파일, 철자확인, 문확인, 계산 프로그램 수식 재계산, 오디오나 비디오의 형변환, 이미지의 썸네일 생성 등이 있다.

  1. CLR 스레드

CLR하나당 스레드풀 1개를 갖는다.

Callback 메서드는 반드시 system.Threading.WaitCallback 델리게이트 타입이어야한다.

Callback 메서드가 처리되지 않은 예외를 발생시키는 경우 clr 프로세스를 종료해버린다.

  1. 실행 컨텍스트

모든 스레드는 각자 실행컨텍스트라는 데이터 구조체를 가지고있다. 보안설정,윈도우 아이덴터티,

호스트설정, 논리 호출 컨텍스트 데이터가 포함되어 있다. 스레드가 코드를 수행할 일부 작업들은 스레드의 실행 컨텍스트 내의 설정 정보의 영향을 받는다.

  1. 협조적인 취소와 타임아웃
  2. 태스크

계산 중심의 비동기 작업을 수행하기 위해서 ThreadPool QueueUserWorkItem 호출하는 방식은 간단한 방법이긴 하지만 많은 제약사항을 가지고 있다. 가장 문제점은 작업 완료시점과 작업 수행 ㅕㄹ과를 얻을 있는 방법을 기본적으로 가지고있지않다. 한계를 극복하기 위해서ms 태스크(TASK)라는 개념을 소개 하였다. 동일 작업의 ThreadPool QueuUserWorkItem 메서드를 대신 Task 사용하면 다음과 같이 바꿀수 있다.

ThreadPool.QueueUserWorkItem(ComputeBoundOp,5); ->

New Task(ComputeBoundOp, 5).Start(); Task.Run(()=> ComputeBoundOp(5)); 모습이 다르긴하나 비동기 작업을 동일하게 수행  

  1. 태스크 완료 취소

태스크가 완료될 때까지 대기하여 그결과를 얻는 방법: net tast t= new tast<int>(); t.start(); t.wait();

Wait 메서드를 호출하게 되면 시스템은 대기하려는 태스크가 이미 수행이 시작되었는지를 확인한다.

수행이 시작되었다면 태스크가 완료될 때까지 wait 메서드를 호출한 스레드를 블로킹하지만 태스크가 수행 이전이라면 wait메서드를 호출 스레드를 이용하여 태스크를 수행할 수도 있다.

계산 중심의 테스크가 수행중에 처리되지 않은 예외를 유발하는 경우, 바로 예외를 발생시키지 않고 특정 컬렉션에 예외를 저장한후, 스레드가 스레드 풀로 반납되도록 한다. 이후 wait메서드를 호출하거나 Result속성 값을 가져오려 하면 그떄 System.AggregateException 예외를 발생시킨다.

Flattern 메서드는 AggregateException 내부의 InnerExceptions 컬렉션 내에 존재할 있는 복잡한 계층 구조를 순차적으로 펼친 새로운 AggregateException 객체를 얻어올수있으며 객체의 InnerExceptions 속성을 통하여 발생하였던 예외들을 순차적으로 순회할 있다. 마지막으로 AggregateExeption Handle이라는 메서드를 제공하는데 AggregateExeption 내부의 예외들이 각각에 대해서 callback 메서드를 호출할수 있다. 이를통해 각각예외들이 처리할수 있는지 아닌지를 살펴볼수있는 기회를 가진다.

  1. 다른태스크 자동실행하기
  2. 차일드 태스크, 태스크내부
  3. 태스크 스케줄러

테스크 하부 구조는 상당히 유연하다, TaskScheduler객가 지원한다. 디버거의 개별 테스크의 정보를 저장하는 역할도 수행한다. FCL에는 테스크스케쥴러를 상속한 스레드 태스크 스케줄러와 동기-컨텍스트 태스크 스케줄러가 포함되어있다. 모든 응용프로그램은 스레드 태스크 스케줄러를 기본값으로 사용한다, 태스크 스케줄러는 스레드 풀의 워커스레드(worker thread) 이용하여 태스크들을 수행하도록 스케줄 한다

  1. 병행 LINQ
  2. 스레드 풀이 스레드를 관리하는 방법

*Timer객체가 가비지로 수집되면, finalization 코드가 타이머를 취소하기 때문에 콜백 메서드가 이상 수행되지 않는다. 따라서 Timer 객체를 사용 할때 Timer 객체가 계속 살아 있도록 변수를 유지하지 않으면 콜백 메서드가 호출되지 않을수도 있다.

스레드 풀의 구현부가 실제로 커스레드와 I/O 스레드 등을 어떻게 관리하는 지에 대해 알아보자.

세부적인 내용들은 CLR 버전 별로 변경어 왔고 앞으로도 바뀔수 있다.

  1. I/O 중심의 비동기 작업

ThreadPool.QueueUser 메서드와 Timer 클래스는 작업 항목을 글로벌 (Global Queue) 추가한다.

워커 스레드가 로벌 큐로부터 작업 항목을 가졀갈 때에는 FIFO순서로 가져온다. 여러개의 워커스레드가 동시에 글로벌 큐에 작업항목을 가져오려고 시도할 수도 있기때문에, 두개 이상의 스레드가 동일한 작업 항목을 가져가지않도록 하기위해서 글로벌 큐는 스레드 동기화 락으로 보호화 되고있다.

워커스레드가 작업을 수행할수 있는 상황이 되면 가장먼저 자신의 로컬 큐를 확인하여 태스크가 존재하는지 확인한다. 만약 로컬 큐에 태스크가 있다면 워커 스레드는 로컬 큐로부터 태스크를 꺼내 수행한다.

여기서 주목할 워커 스레드가 로컬 큐로부터 태스크를 꺼내올때는 LIFO 순서로 꺼내온다. 로컬 큐의 가장 앞쪽은 큐를 소유하고 있는 워커 스레드 접근할수 있도록 구성해뒀기떄문에, 큐의 앞쪽 태스크를 추가하거나 이로부터 태스크를 가져오는 과정에어서 어떠한 동기화 락도 필요하지 않으며 작업을 빨리 수행 있다. 하지만 이렇게 동작하도록 구현하다 보니 삽입된 태스크를 역순으로 수행하게되었다.

*스레드 풀은 큐잉된 작업 항목의 수행 순서를 보장하지 않는다.

만일 워커 스레드가 로컬 큐가 비어있음을 확인하였다면 이번에는 다른 워커 스레드의 로컬큐에 태스크가 있는지 확인하고, 태스크가 있는 경우 태스크를 탈취해온다. 이와같은 태스크 탈취과정은 항상 로컬 큐의 가장 (tail)에서만 일어나고, 스레드 동기화 락으로 보호 되어있어서 성능은 그다지 좋지 않다.

  1. 단순 스레드 동기화 요소
  1. 유저모드 동기화요소
    1. Volatile 동기화 요소
    2. Interlocked 동기화 요소

이는  단순 데이터 타입의 변수에 대하여 지정된 시간에 원자적으로 변수의 값을 읽고 쓸수 있도록 해준다.

스레드 내의 공유 변수를 컴파일러가 최적화하면 한번만 확인하게된다. 루프를 반복하는 과정에서 지속적으로 bool값을 확인하지 않게된다.

Volatile.write 메서드는 호출한 위치에서 값이 반드시 쓰여질 것임을 보장한다. 또한 프로그램 코드의 순서상 이코드를 위치보다 앞쪽에서 수행된 로드(load)/스토어(store) 과정은 반드시 코드보다 앞서 수행될 것임을 보장한다.

Volatile.Read 메서드는 메서드를 호출한 위치에서 값이 읽혀질 것임을 보장한다.

또한 프로그램 코드의 순서상 이코드 이후에 위치한 로드(load),/스토어(store) 과정은 반드시 메서드가 수행된 이후에 될것임을 보장한다.

JI컴파일러는 volatile 선언된 필드를 사용하는 코드를 컴파일 할때 Volatile.Read Volatile.Write 메서드를 사용하도록 해주기 떄문에, 개발자가 명시적으로 사용할 필요가 없다. 또한 이키워드를 사용하면 c#컴파일러와 JIT컴파일러에게 필드의 값을 CPU 레지스터로 캐싱하지 않도록 하여 ,항상 메모리로부터 값을 일고 쓰도록 한다.

Interlocked 클래스의 메서드를 이용하면 원자적으로 값을 읽고 뿐만 아니라, 완전히 메모리 펜스(memory fence)기능을 제공한다. 메모리 펜스란 Interlocked 메서드를 호출 이후에 수행될 것임을 보장하는것을 말한다.

 

  1. 동기화 종류, 뮤텍스, 크리티컬섹션, 상호배제, Interlocked ,스핀락

(1) Locking으로 공유 리소스에 대한 접근을 제한하는 방식으로 C#

lock, Monitor, Mutex, Semaphore, SpinLock, ReaderWriterLock 등이 사용되며,

(2) 타 쓰레드에 신호(Signal)을 보내 쓰레드 흐름을 제어하는 방식으로

AutoResetEvent, ManualResetEvent, CountdownEvent 등이 있다.

 

 

 

 

 

  • Lock 종류
    • Spinlock :

잠금을 획득하기 위해 대기하는 동안 회전하는 하위 수준의 동기화 기본 형식입니다. 멀티 코어 컴퓨터에서 대기 시간이 짧은 것으로 예상되고 경합이 최소인 경우 SpinLock의 성능이 다른 종류의 잠금보다 더 뛰어납니다. 그러나 프로파일링을 통해 System.Threading.Monitor 메서드 또는 Interlocked 메서드가 프로그램의 성능을 크게 낮추는 것을 확인하는 경우에만 SpinLock을 사용하는 것이 좋습니다.

 

 SpinLock을 사용하면 스레드가 매우 짧은 시간 범위 이상으로 잠금을 보유할 수 없고 잠금을 보유하는 동안 스레드가 차단할 수 없습니다.

 

  • Mutext

Mutex 클래스는 Monitor클래스와 같이 특정 코드 블럭(Critiacal Section)을 배타적으로 Locking하는 기능을 가지고 있다. 단, Monitor클래스는 하나의 프로세스 내에서만 사용할 수 있는 반면, Mutex 클래스는 해당 머신의 프로세스간에서도 배타적 Locking을 하는데 사용된다. Mutex 락킹은 Monitor 락킹보다 약 50배 정도 느리기 때문에 한 프로세스내에서만 배타적 Lock이 필요한 경우는 C#의 lock이나 Monitor 클래스를 사용한다. 

  • Monitor

Monitor 클래스는 C#의 lock과 같이 특정 코드 블럭(Critiacal Section)을 배타적으로 Locking하는 기능을 가지고 있다. Monitor.Enter() 메서드는 Critiacal Section 블럭을 시작하여 한 쓰레드만 블럭으로 들어가게 하며, Monitor.Exit()은 락킹을 해제하여 다음 쓰레드가 Critiacal Section 블럭을 실행하게 한다. C# lock은 실제로는 Monitor.Enter()와 Monitor.Exit()을 사용하여 Critiacal Section을 try...finally 문으로 감싼 문장들로 컴파일시 코드를 변경하는 것이다.

 

Wait() 메서드는 현재 쓰레드를 잠시 중지하고, lock을 Release한 후, 다른 쓰레드로부터 Pulse 신호가 올 때까지 대기한다. Wait에서 lock 이 Release 되었으므로 다른 쓰레드가 lock을 획득하고 작업을 실행한다. 다른 쓰레드가 자신의 작업을 마치고 Pulse() 메서드를 호출하면 대기중인 쓰레드는 lock을 획득하고 계속 다음 작업을 실행한다.

 

Pulse() 메서드가 호출되었을 때, 만약 대기중인 쓰레드가 있다면, 그 쓰레드가 계속 실행하게 되지만, 만약 대기중인 쓰레드가 없다면, Pulse 신호는 없어진다.

  • Semaphore

Semaphore 클래스는 공유된 리소스를 지정된 수의 쓰레드들만 엑세스할 수 있게 허용하는데, 예를 들어 최대 10개의 쓰레드들이 엑세스하도록 허용하였다면, 11번째 쓰레드는 현재 사용 중인 10개의 쓰레드중 누군가가 리소스 사용을 마쳐야지만, 그 리소스를 사용할 수 있게 된다. lock, Monitor, Mutex가 한번에 한 쓰레드만을 허용하는 반면, Semaphore는 복수 개의 쓰레드가 동시에 리소스를 엑세스하는 것을 허용한다.

  • Interlocked

원자 단위 연산으로 지정된 변수를 증가시키고 결과를 저장합니다.

Interlocked 클래스는 int 형 값을 증가시키거나 감소시키는데 사용한다. 멀티 쓰레드 환경에서 하나의 int 형 전역 변수를 공유한다고 생각해보자. 이런 경우에 A 쓰레드와 B 쓰레드가 값을 동시에 읽어와서 B 쓰레드가 수정한 값을 저장하고, A 쓰레드가 다시 수정한 값을 저장하게 되면 B 쓰레드의 변경사항을 잃어버리게 된다.

 

지금까지 이러한 자원의 동기화를 위해서 모니터나 뮤텍스를 사용하는 방법을 설명했지만 간단한 int 형의 값을 여러 쓰레드가 공유하는 것이 일반적이기 때문에 이러한 작업을 캡슐화한 클래스를 제공한다.

메소드 이름

설 명

CompareExchange

두 대상을 비교하여 값이 같으면 지정된 값을 설정하고, 그렇지 않으면 연산을 수행하지 않는다.

Decrement

지정된 변수의 값을 감소시키고 저장한다.

Exchange

변수를 지정된 값으로 설정한다.

Increment

지정된 변수의 값을 증가시키고 저장한다

  • AutoResetEvent

AutoResetEvent는 이 이벤트를 기다리는 쓰레드들에게 신호를 보내 하나의 쓰레드만 통과시키고 나머지 쓰레드들은 다음 신호를 기다리게 한다. 이는 흡사 유료 주차장 자동 게이트와 같이 한 차량이 통과하면 자동으로 게이트가 닫히는 것과 같다. 쓰레드 A가 AutoResetEvent 객체의 WaitOne() 메소드를 써서 대기하고 있다가, 다른 쓰레드 B에서 이 AutoResetEvent 객체의 Set() 메서드를 호출하면, 쓰레드 A는 대기 상태를 해제하고 계속 다음 문장을 실행할 수 있게 된다.

  • ManualResetEvent

ManualResetEvent는 하나의 쓰레드만 통과시키고 닫는 AutoResetEvent와 달리, 한번 열리면 대기중이던 모든 쓰레드를 실행하게 하고 코드에서 수동으로 Reset()을 호출하여 문을 닫고 이후 도착한 쓰레드들을 다시 대기토록 한다. 아래는 여러 쓰레드의 실행을 중지시킨 후, ManualResetEvent로 신호를 보내 대기중이던 모든 쓰레들들을 한꺼번에 실행시키는 예제이다.

  • CountdownEvent

ManualResetEvent가 한 쓰레드에서 신호(Signal)을 보내 복수 쓰레드들을 통제하는데 사용되는 반면, .NET 4.0에 소개된 CountdownEvent는 한 쓰레드에서 복수 쓰레드들로부터의 신호들을 기다리는데 사용된다. 아래는 10개의 쓰레드를 시작한 후, 이 쓰레드들로부터 처음 5개의 신호가 (CountdownEvent.Signal() 메서드) 먼저 도착하는 대로 메인쓰레드는 Wait 대기 상태를 해제하고 사용한다.

 

 

 

'C#, ASP.NET, CORE, MVC' 카테고리의 다른 글

ASP.NET 웹폼  (0) 2020.02.26
ASP.NET 이란  (0) 2020.02.26
[C#] 기타용어 및 개념  (0) 2018.07.20
[C#] LINQ - 제한 연산자  (0) 2018.07.20
[C#] Lamda(람다식)  (0) 2018.07.20

+ Recent posts