- Part 1 Getting Started
- Part 2 Basic Synchronization
- Part 3 Using Threads
- Part 4 Advanced Threading
- Part 5 Parallel Programming
Part 1: Getting Started
소개 및 개념.
C#은 멀티쓰레딩을 통해서 코드의 병렬 실행을 지원합니다. 쓰레드는 다른 쓰레드와 동시에 동작 할 수 있는 독립적인 실행 경로입니다.
C# 클라이언트 프로그램(Console, WPF, or Windows Forms)은 CLR과 운영체제에 의해서 자동적으로 단일 쓰레드를 생성하고 실행됩니다("메인" 쓰레드), 그리고 부가적인 쓰레드들을 생성하면서 멀티쓰레딩 환경이 만들어 집니다.
여기 간단한 예제와 그 출력값이 있습니다:
모든 예제들은 다음 네임 스페이스가 임포트 되었다고 가정합니다:
using System;
using System.Threading;
class ThreadTest
{
static void Main()
{
Thread t = new Thread (WriteY); // 새로운 쓰레드 시작
t.Start(); // WriteY() 함수 구동
// 동시적으로, 메인쓰레드에서 뭔가를 한다.
for (int i = 0; i < 1000; i++) Console.Write ("x");
}
static void WriteY()
{
for (int i = 0; i < 1000; i++) Console.Write ("y");
}
}
메인쓰레드는 문자 "y"를 반복적으로 출력하는 메소드를 구동하는 새로운 쓰레드 t를 생성합니다. 동시에, 메인쓰레드는 문자 "x"를 반복적으로 출력합니다:
프로그램이 시작하면, 쓰레드가 끝나는 지점까지 쓰레드의 IsAlive 속성이 true를 리턴합니다. 쓰레드는 전달된 위임 쓰레드의 생성자 실행을 마쳤을때 종료됩니다. 쓰레드가 종료되었을때, 쓰레드는 다시 시작할수 없습니다.
CLR은 각 쓰레드에 로컬 변수들을 독립적으로 저장할 수 있도록 자신만의 메모리 스택을 할당합니다. 다음 예제에서, 우리는 지역변수를 가진 메소드를 정의하고, 메인 쓰레드에서 동시에 쓰레드를 호출하고 새로이 쓰레드를 생성합니다:
static void Main()
{
new Thread (Go).Start(); // 새로운 쓰레드 상에서 Go() 메소드 호출
Go(); // 메인 쓰레드 상에서 Go() 메소드 호출
}
static void Go()
{
// 지역변수인 "cycles"를 선언하고 사용한다
for (int cycles = 0; cycles < 5; cycles++) Console.Write ('?');
}
cycles 변수의 독립적인 복사본이 각 쓰레드의 메모리 스택에 생성되어 지고, 출력은 예상과 같이 열개의 물음표 입니다.
쓰레드는 같은 오브텍트 인스턴스의 일반적인 참조를 가진다면 데이터를 공유합니다. 예를 들어:
class ThreadTest
{
bool done;
static void Main()
{
ThreadTest tt = new ThreadTest(); // 일반 인스턴스 생성
new Thread (tt.Go).Start();
tt.Go();
}
// 주의 Go 메소드는 이제 인스턴스 메소드이다.
void Go()
{
if (!done) { done = true; Console.WriteLine ("Done"); }
}
}
여기 동일한 ThreadTest 인스턴스 상에서 두개 쓰레드가 Go() 메소드를 호출하기 때문에, 두 쓰레드는 done 필드를 공유하기 때문에, 출력 결과인 "Done"이 두번 출력되는 것이 아니라 한번 출력 됩니다.
Static 필드는 쓰레드 간의 데이터를 공유하는 다른 방식의 접근을 가집니다. 여기 done을 static 필드를 사용한 동일한 예제가 있습니다:
class ThreadTest
{
static bool done; // Static 필드는 모든 쓰레드들 간에 공유된다.
static void Main()
{
new Thread (Go).Start();
Go();
}
static void Go()
{
if (!done) { done = true; Console.WriteLine ("Done"); }
}
}
예제들은 서로 다른 주요 개념을 보여 줍니다 : 쓰레드 세이프한 것(혹은, 부족한것!) 출력은 정확히 확신할 수 없습니다: “Done”변수가 두번 출력될 수 있을 가능성이 있을 것 같습니다. 그러나 만약 우리가 Go 메소드 안의 구문의 순서를 바꾸면, 두번 출력 된“Done”문자열이은 극적으로 그 이상 출력 될 공산이 큽니다.
static void Go()
{
if (!done) { Console.WriteLine ("Done"); done = true; }
}
이 문제는 한 쓰레드가 if 문을 바로 평가하기전에 다른 쓰레드가 WriteLine 문을 실행 할 때 발생합니다 - 이 쓰레드가 done 변수를 true로 세팅하기 전에
일반 필드를 읽거나 쓰려고 하는 동한 exclusive lock을 얻는 것, 이것이 이 문제의 처방전일 것입니다.
C#은 단지 이 목적을 위해서 lock 구문을 준비하고 있습니다.
class ThreadSafe
{
static bool done;
static readonly object locker = new object();
static void Main()
{
new Thread (Go).Start();
Go();
}
static void Go()
{
lock (locker)
{
if (!done) { Console.WriteLine ("Done"); done = true; }
}
}
}
두개의 쓰레드가 동시에 lock을 얻기위해서 경쟁할때(이경우 locker), 이 lock이 풀릴때 까지 한 쓰레드는 기다리거나, 블록됩니다. 이경우에, 단 하나의 쓰레드만이 한번에 코드의 critical section에 들어 갈 수 있습니다, 그리고“Done”문자열은 단지 한번 출력 될 것입니다. 이런 형식으로 보호되는 코드 - 멀티쓰레딘 컨텍스트에서의 불확적성으로부터 - 를 thread-safe라 부릅니다.
공유되는 데이터는 멀티쓰레딩 환경에서 복잡하고 잘 알려지지 않은 주된 에러를 일으킵니다. 비록 기본적인 얘기긴 하지만, 가능한한 간단하게 만들도록 주의를 기울여야 합니다.
블록되어 있는 쓰레드는 CPU 자원을 소비하지 않습니다.
Join and Sleep
다른 쓰레드가 끝날때 까지 기다리는 것은 Join 메소드 호출로서 가능해 집니다. 예를 들어:
static void Main()
{
Thread t = new Thread (Go);
t.Start();
t.Join();
Console.WriteLine ("Thread t has ended!");
}
static void Go()
{
for (int i = 0; i < 1000; i++) Console.Write ("y");
}
이 예제는 “y”를 1,000번 출력하고 “Thread t has ended!”라는 문자가 그 후 바로 출력되는 예제 입니다. Join 메소드를 호출 할때 타임아웃값을 밀리세컨드 혹은 TimeSpan 형식으로 줄 수 있습니다. 그리고 만약 쓰레드가 끝나면 true를, 타임아웃이 되면 false를 리턴합니다.
Thread.Sleep 일정 기간동안 현재 쓰레드를 멈춥니다:
Thread.Sleep (TimeSpan.FromHours (1)); // sleep for 1 hour
Thread.Sleep (500); // sleep for 500 milliseconds
Sleep 혹은 Join으로 기다리고 있을 동안 쓰레드는 블럭되고 CPU 리소스를 소비하지 않습니다.
Thread.Sleep(0) 메소드는 즉시 스레드의 현재 타임 슬라이스를 포기하고 자발적으로 다른 스레드에게 CPU를 넘겨줍니다 .
프레임 워크 4.0의 새로운 Thread.Yield () 메소드는 같은 기능을 하나, 동일 프로세서에서 실행되는 스레드에만 양도하는 것을 제외 합니다.
Sleep(0) 또는 Yield 메소드는 고급 성능 조정을 위한 제품 코드에 때때로 유용합니다. 또한 스레드 안전 문제를 발견하는 데 도움을 주는 훌륭한 진단 도구입니다 :
만약 Thread.Yield () 메소드를 코드 어디에 삽입하여 프로그램을 나누거나 만드는 경우, 거의 확실히 버그가 만드는 것입니다.
쓰레드는 어떻게 동작할까?
다중 쓰레딩은 내부적으로 일반적으로 운영 체제로 부터 위임 받은 CLR(Common Language Runtime)의 기능인 쓰레드 스케줄러 의해 관리됩니다.
스레드 스케줄러는 모든 활성 스레드가 적당한 실행 시간을 할당받으며, 대기하거나 블록 당한 스레드가(예를 들어, 배타적 잠금 또는 사용자 입력) CPU 시간을 소비하지 않도록하는 보장합니다.
쓰레드 스케줄러가 타임 슬라이스를 수행하는 단일 프로세서 컴퓨터에서 - 활성 스레드들간의 스위칭은 빠르게 일어납니다. Windows 하에서, 타임 슬라이스는 일반적으로 수만의 밀리 초 범위에서 - 실제로 CPU 오버 헤드보다 훨씬 더 큰 하나의 스레드와 다른 쓰레드 (몇 - 마이크로 영역에서 일반적이다) 사이의 컨텍스트 스위칭이 일어납니다 .
다중 프로세서 컴퓨터에서, 멀티쓰레딩은 다른 스레드가 다른 CPU에서 동시에 코드를 실행 하도록, 시분할 방식(time-slicing)과 동시성(genuine concurrency)방식의 혼합된 형태로 구현됩니다. 뿐만 아니라 다른 응용 프로그램의 같은 - 여전히 운영체제 자신의 쓰레드들 혹은 다른 애플리케이션들을 서비스 할 필요가 있기때문에 있기 때문에 여전이 약간의 시분할 방식을 여전히 사용 할 것입니다.
시분할 같은 외부 요인에 의해서 스레드는 실행을 중단할 때 선점 된다고 합니다. 쓰레드는 언제 어디서 선점 할 수 있는 조절 기능은 없습니다.
쓰레드와 프로세스
쓰레드는 응용 프로그램이 실행되는 운영 체제 프로세스와 비슷합니다. 프로세스가 컴퓨터에서 병렬로 실행하는 것처럼, 쓰레드는 하나의 프로세스 내에서 병렬로 실행됩니다. 프로세스는 완전히 서로 분리되며 쓰레드들은 이 분리 등급으로 제한을 받습니다. 특히 응용 프로그램에서 실행하는 다른 스레드와 (힙) 메모리를 공유합니다 . 이것이 쓰레딩이 유용한 이유 부분입니다 : 예를 들면, 데이터가 들어오는 데로 다른 스레드가 데이터를 표시 할 때 또 하나의 스레드는 백그라운드에서 데이터를 가져올 수 있습니다.
스레딩의 사용과 오용
다중 스레딩은 많은 사용법이 있습니다, 그중 가장 일반적인은 다음과 같습니다 :
응답성이 뛰어난 사용자 인터페이스를 얻기
병렬 "작업자"스레드에서 시간이 많이 걸리는 작업을 실행하여 메인 UI 스레드 처리 키보드와 마우스 이벤트를 계속 프로세싱 작업을 할 수 있습니다.
다른 블록 된 CPU를 효율적으로 사용하기
멀티쓰레딩은 쓰레드가 다른 컴퓨터나 하드웨어 자원으로 부터 응답을 기다릴때 유용합니다. 한쓰레드가 작업을 수행하는 동한 블록될때, 다른 쓰레드들은 다른 부담없는 컴퓨터를 활용할 수 있습니다.
병렬 프로그래밍
여러 스레드들간에 부하가 공유된다면 "분할 정복 전략"(제 5 장 참조)으로 집중적인 계산을 수행하는 코드는 멀티 코어 또는 멀티 프로세서 컴퓨터에서 더 빠르게 실행할 수 있습니다.
추측 실행
멀티 코어 시스템에서, 때때로 뭔가를 예측하고 미리를 수행하여 성능을 향상시킬 수 있습니다. LINQPad는 새 쿼리의 생성을 빠르게하기 위해 이 기술을 사용합니다. 차이는 모두 같은 작업을 해결하는 몇몇의 다른 알고리즘을 실행하는 것입니다. 어떤것이라도 최초의 "승리"를 완료 하기만하면 - 만약 당신이 앞서 어떤 알고리즘이 가장 빠른지 알 수 없을 때 이 방법이 효과적입니다.
요청들을 동시 처리 하기
서버에서 클라이언트 요청이 동시에 도착하므로 (당신은 ASP.NET, WCF, 웹 서비스 또는 원격을 사용하는 경우. NET 프레임 워크는 이것에 대한 스레드를 만듭니다) 병렬로 처리 할 필요가 있습니다.
또한 이방식은 클라이언트에서 유용 할 수 있습니다(사용자로부터도 여러 요청 예를 들어, peer-to-peer 네트워킹 처리 혹은 사용자로부터 다중 요청 처리).
ASP.NET 및 WCF와 같은 기술을 사용한다면 만약 당신이 적절한 락킹, 쓰레드가 충돌하도록 구동하도록 공유 데이터를 (아마도 정적 필드를 통해) 액세스하지 않는 한, 멀티 쓰레딩도 발생하고 있음을 인식 할 수 없을것입니다.
스레드는 여러가지 끈들을 가지고 있습니다. 이 중 최고는 멀티 스레딩의 복잡성을 증가시킬 수 있다는 것입니다. 쓰레드를 많이 갖는 것 자체만으로도 많은 복잡성을 만들어지지 않을수 없습니다 이것이 스레드 사이의 상호 작용(일반적으로 공유 데이터를 통해)이라는 것입니다. 이 상호 작용이 의도적인지 여부를 알아봐야하고, 긴 개발주기 상에 영향을 미치며, 간헐적이면서 재생산 할 수없는 버그를 일으킬수 지속적인 취약점을 안겨 줍니다. 이러한 이유로, 그것은 최소한의 상호 작용을 유지하고, 가능한 간단하고 검증 된 디자인에 충실해야 합니다. 이 글은 이러한 복잡성을 다루는에 주로 초점을 맞추고 상호 작용을 제거한다는 말보다 더 좋은 말은 없다는것입니다
좋은 전략은 독립적으로 검사하고 테스트 할 수있는 재사용 가능한 클래스로 멀티 스레드 로직을 캡슐화하는 것입니다.프레임 워크 자체는 우리가 나중에 다룰 많은 높은 수준의 스레딩 구조를 제공합니다.
스레딩은 스레드 (CPU 코어보다 더 활성 스레드가있을 때) 예약 및 스위칭 자원 및 CPU 비용을 초래 하며 생성 / TEAR-DOWN 비용도있다. 다중 스레딩은 항상 응용 프로그램의 속도를 높여주지 않습니다 - 과도하거나 부적절하게 사용하는 경우 심지어 그것을 속도가 느려질 수 있습니다. 예를 들어, 큰 디스크 I / O 작업이 포함되면 10 스레드를 가지고하는 작업 하는것보다 순차적으로 몇개의 작업자 스레드가 실행 작업을 실행하도록 하는 것이 빠를 수 있습니다. (Wait와 Pulse로 신호를 보내는 것에 대해, 단순히 이 기능을 제공하는 생산자 / 소비자 큐을 구현하는 방법에 대해 설명합니다.)
계속.
'프로그래밍' 카테고리의 다른 글
윈도우 SDK와 커맨트 프롬프트를 이용한 C++ 프로젝트 컴파일하기 (0) | 2023.02.26 |
---|---|
[윈32 프로그래밍]이클립스와 Windows SDK 7으로 해보기 (0) | 2023.02.25 |
Mosquitto와 Paho 클라이언트라이브러리를 사용 (0) | 2023.02.22 |
DLL 인젝션 - 간단한 예제 - notepad (0) | 2023.02.21 |
열거 유형 - 정량적 연구 (0) | 2023.02.20 |