Search

'VSTS 2010 공식 팀 블로그'에 해당되는 글 41건

  1. 2011.11.14 [KGC 2011] 발표 자료 (4)
  2. 2011.10.05 [StartD2D-9] 디바이스 로스트( Device Lost )와 WDDM( Windows Display Driver Model ) (2)
  3. 2011.09.28 [StartD2D-8] 투명 이미지 다루기 (2)
  4. 2011.07.11 [StartD2D-5] Direct2D의 리소스 기본 개념.
  5. 2011.05.09 [JumpToDX11-22] DirectX11의 테셀레이션 ( 간단한 테셀레이션 샘플편 ) (2)
  6. 2011.04.11 [JumpToDX11-21] DirectX11의 테셀레이션 ( Domain Shader 의 역할편 )
  7. 2011.03.17 [StartD2D-1] Good-bye~~ GDI… (2)
  8. 2011.01.25 [JumpToDX11-19] DirectX11의 테셀레이션 ( Hull Shader 역할편 )
  9. 2010.08.26 [JumpToDX11-15] DirectX9 세대의 테셀레이션( IDirect3DDevice9::DrawXXXPatch편 )
  10. 2010.08.16 Expression Blend와 함께 하는 윈도우폰7 개발 이야기 - 2편 - (4)
  11. 2010.08.16 Expression Blend와 함께 하는 윈도우폰7 개발 이야기 - 1편 -
  12. 2010.07.19 [JumpToDX11-14] DirectX9 세대의 테셀레이션( ID3DXPatchMesh 편 )
  13. 2010.06.28 Welcome to Parallel C#(10) - 이보게, 잠깐 뒤를 돌아보지 않겠나.
  14. 2010.06.22 Welcome to Parallel C#(9) - 백지장은 맞들지 말엉.
  15. 2010.06.17 Welcome to Parallel C#(8) - 취소 쉽게 하기.
  16. 2010.06.14 Welcome to Parallel C#(7) - Excuse me.
  17. 2010.06.10 Welcome to Parallel C#(6) - To be continue...
  18. 2010.06.08 Welcome to Parallel C#(5) - 병렬작업에서 예외가 생기면 어케...?
  19. 2010.06.03 Welcome to Parallel C#(4) - 작업의 기본 Part 2.
  20. 2010.05.31 Welcome to Parallel C#(3) - 작업의 기본.

[KGC 2011] 발표 자료

DirectX 11 2011.11.14 08:00 Posted by 조진현


안녕하세요~ 조진현입니다.
얼마 전 대구에서 KGC 2011 행사가 있었습니다.
저는 그 곳에서 DirectX11 과 관련한 발표를 진행하고 왔습니다.
그래서 발표 슬라이드를 공개해 드립니다.

특히나 이번 발표 때, C++ AMP 에 대한 언급이 있었습니다.
AMP는 아래 링크에 흥배님이 자세히 설명해 주셨습니다.
http://vsts2010.net/591

빠른 시간 내에 발표 때 언급했던 C++ AMP 와 관련한 내용을
팀 블로그에 게재하도록 하겠습니다..^^

[조진현] [Kgc2011]direct x11 이야기
View more presentations from 진현 조.
신고

디바이스 로스트( Device Lost ) 라는 용어에 대해서 친숙한 분도 계실 것이고,
그렇지 않은 분도 계실 것입니다.
아마 Direct3D 를 다루어 보신 분들은 이 용어에 무척이나 친숙할 것입니다.

디바이스 로스트라는 것은 특정 상황을 얘기하는 것입니다.
이 상황은 지금까지 확보하고 있던 메모리 같은 리소스들이 모두 사라져서,
아무 작업도 할 수 없는 상황을 얘기합니다.
즉, 시스템 리소스가 모두 무효화 되어버린 상황이라 할 수 있습니다.

GDI 를 사용해서 렌더링 한다면, 갑자기 HDC 가 비활성화 되어버린 것이라 생각할 수 있습니다.
게임 개발에서 주로 사용하는 Direct3D를 사용하는 경우에는 Alt + Tab 문제라던지,
실시간으로 화면 사이즈를 조절하게 되었을 때 주로 나타납니다.
Direct3D로 개발하는 경우에는 예전부터 이 문제에 친숙했기 때문에,
이 상황에 대해서 리소스들을 복구하는 코드를 개발자들이 직접 작성해 주었습니다.

Direct2D도 DirectX의 하나이기 때문에 바로 이 디바이스 로스트 상황이 발생을 합니다.
그래서 개발자들이 이 상황에 대해서 직접 적절한 처리를 해주어야 합니다.

그런데 중요한 것은 현재의 디바이스 로스트 상황이 예전과는 다르다는 것입니다.
이 차이를 설명하려면 디스플레이 드라이버 모델까지 언급을 해야 합니다.

Windows XP 시대와 현재의 Windows 7 시대는 디스플레이 드라이버 모델이 다릅니다.
XP 시대는 XPDM 이라는 드라이버 모델을 사용하고 있으며,
윈도우 7 시대에는 WDDM( Windows Display Driver Model ) 라는 모델을 사용하고 있습니다.
( 모니터에 그리기까지의 과정이 다르다고 생각하시면 좋을 것 같습니다.^^ )
이 모델의 차이가 디바이스 로스트에서도 차이를 만들어 냅니다.

XPDM 은 XP때까지 Windows OS가 발전시켜온 드라이버 모델입니다.
즉, 옛 것을 꾸준히 업데이트 한 결과라 할 수 있습니다.
아무래도 너무 오래되다보니 복잡하고 난해한 구조가 되었고,
그 복잡성 때문에 버그들이 드라이버 내부에 있었다고 합니다.

또한 하드웨어의 발전에도 빠르게 대응하는 것에 굉장한 한계가 있었다고 합니다.

그래서 Windows Vista 때 이 문제를 극복하기 위해서 도입된
새로운 디스플레이 드라이버 모델이 바로 WDDM 입니다.
WDDM 은 기존의 복잡성을 최대한 버려서 심플한 구성을 가지도록 설계되었습니다.
기존의 XPDM 이 커널모드에서 대부분의 처리를 수행했던 것을
커널모드와 사용자 모드로 분리를 시켜서 이 디바이스 로스트에 대한 상황에 대한
대응을 용이하게 했습니다.

XPDM의 경우에는 커널에서 문제가 발생하면,
이 때문에 시스템 전체를 다시 시작해야 하는 불상사(?)가 꽤 있었습니다.
MS 통계에 따르면 우리가 흔히 보던 블루스크린 상황의 20% 정도가
바로 이 디스플레이 드라이버가 원인이였다고 합니다.
그렇기 때문에 간략화 된 커널 모드의 드라이버를 제공하고,
계산이 복잡한 대부분의 기능은 사용자 모드 드라이버로 이동시킴으로써
문제를 해당 애플리케이션 하나로 국한 시킬 수 있습니다.
이는 블루스크린 상황을 감소시킬 수 있는 하나의 방법이기도 합니다.

XP OS의 디스플레이 드라이버 모델 교체가 어렵기 때문에
WDDM 은 Vista 이상의 OS에서만 지원합니다.
결국 이는 최신의 DirectX 들이 XP OS 에서 정상적으로 작동하지 못하는
가장 근본적인 이유입니다.

WDDM과 XPDM 의 차이는 바로 GPU 활용에 있습니다.
XPDM 의 경우에는 GPU 관련 처리를 할 수 없었습니다.
왜냐하면 옛것을 꾸준히 계승시켜 발전시킨 모델이였기 때문에,
GPU 처리와 같은 큰 패러다임의 전환은 이 모델 자체를 변경하는 일이기 때문입니다.
현재 Vista 이후의 Windows OS에서는 대부분의 그래픽 작업과 윈도우 관리에
바로 이 GPU가 활용되고 있습니다.

XP 시대에서 DirectX를 사용하는 것은 GPU를 독점적으로 사용하는 작업이였습니다.
만약에 GPU 작업을 실행하는 중에 다른 애플리케이션에서 GPU를 사용하려 한다면,
GPU의 제어 권한을 빼앗기게 됩니다.
XP 시대에서는 GPU를 여러 애플리케이션에서 동시에 공유할 수가 없었습니다.

하지만 현 세대의 Windows OS에서는 GPU 메모리 관리자가 비디오 메모리를 관리하고 있으며,
GPU 스케줄러에 의해서 스케줄을 조정합니다.
우리가 '빠르다' 라고 하는 작업의 뒤에 바로 이런 작업들이 이루어지고 있습니다.

이러한 GPU 활용의 적극적인 도입은 결국 퀄리티의 향상까지 연결됩니다.
XP 시대의 XPDM 의 경우에는 GDI로 렌더링 작업을 할 때,
화면에 직접 그리는 개념으로 작업을 했습니다.
그렇기 때문에 윈도우를 이동하거나 사이즈를 조절하게 되면,
화면이 깜빡이는 것을 확인 할 수 있었습니다.
( 티어링과 같은 현상도 확인할 수 있었습니다.^^ )

또한 이 때 대량의 WM_PAINT 메시지가 발생해서 시스템에 상당한 부하를 주기도 했었습니다.
이는 XPDM 의 경우 모니터 화면에 직접 렌더링 작업을 했었기 때문입니다.

비디오 메모리가 풍족해진 현 세대에서는 오프-스크린 버퍼를 두어서,
화면이 아닌, 다른 버퍼에 렌더링 작업을 수행합니다.
( 흔히들 얘기하시는 더블-버퍼링 기법입니다.^^ )

이러한 것을 담당하는 것이 DWM( Desktop Window Manager ) 입니다.
이는 OS 에서 자동적으로 실행하기 때문에 쉽게 확인할 수 있습니다.


DWM 은 간단히 말해서 DirectX 애플리케이션입니다.
DWM 은 비디오 메모리를 만들어서 각 애플리케이션 화면을 모아서
우리에게 화면을 보여주는 역할을 합니다.
그렇기 때문에 DWM 은 오프-스크린 버퍼를 관리하는 기능을 가지고 있습니다.

WDDM 에 대해서 논할 내용도 상당히 많이 있습니다.
Vista 시절에는 WDDM 1.0 이였고,
Windows 7 에서는 WDDM 1.1 이 사용되고 있습니다.
역시 버전이 업데이트 되면서, 더 좋아진 부분이 있습니다.
1.1 에 관한 개선 사항은 아래의 링크로 대신합니다.^^
http://jacking.tistory.com/442


GPU를 활용한 많은 기능들이 현세대의 Windows OS에 기본적으로 탑재가 되어 있습니다.
그렇기 때문에 디바이스-로스트 같은 GPU 관련한 예외 상황들에 대한 처리를
개발자들이 해주어야 합니다.
XPDM의 경우와 개념도 다르고, 발생되는 상황도 다릅니다.
Direct2D의 경우 디바이스-로스트란,

그래픽카드가 일정시간 동안 응답을 하지 않을 때를 의미합니다
.
여기서 일정 시간이란 기본적으로 2초로 설정되어 있다고 합니다.
Direct2D 에서 디바이스-로스트는 잘 발생되지는 않습니다.^^

이와 관련한 실제 처리는 다음 시간에 계속하겠습니다.^^

신고

[StartD2D-8] 투명 이미지 다루기

DirectX 11 2011.09.28 08:00 Posted by 조진현


이번 시간에 다룰 것은 투명 이미지 입니다.
얼마 전 댓글로 문의하신 내용인데 답변을 달기가 조금 부족한 듯 싶어서,
이렇게 별도로 아티클(?)로 남깁니다.

예전에 Win32 API 로 알파가 있는 이미지를 표현하는 작업은 무척 번거로운 작업이였습니다.
이미지 색상에 알파가 고려되어지면, 각 색상 성분마다 알파 연산을 해주어야 합니다.
또한 RGB 각 성분이 8비트씩 사용하는데 A성분이 추가되어지면서,
다시 8비트의 추가 데이터들이 각 색상값들에 필요하게 됩니다.
용량이 커지면 성능에 문제가 생기게 되는 것은 당연한 일입니다.

그래서 이를 흉내내기 위한 대안으로 마련된 것이 ColorKey 라고 불리는 기법입니다.
이 기법은 이미지 내의 특정 색상을 표현하지 않음으로써 구현됩니다.
아래의 그림을 예로 들어보겠습니다.



이미지 내에서 배경이 모두 붉은 색으로 되어있습니다.
이런 경우에 붉은 색을 ColorKey로 지정해서 데이터를 읽지 않는 것입니다.
그러면, 캐릭터 관련 색상만 메모리에 기록되게 됩니다.
이 방법을 사용하면 24비트 비트맵만으로 캐릭터를 표현할 수 있습니다.
주의해야 할 점은,
ColorKey 에 해당하는 색상 값을 아티스트들에게 사용하지 말 것에 대한
사전 협의가 있어야 겠지요.

아쉽게도(?) Direct2D에서 이 ColorKey 사용에 대한 API를 찾지 못했습니다.
( Direct3D 에는 있습니다.^^ )

사실 ColorKey 방식이 널리 이용되긴 하지만,
근본적으로 알파 처리를 이용하며 관련 효과를 모두 구현할 수 있습니다.
그렇기 때문에, 굳이 ColorKey 를 염두할 필요성은 없습니다.

이번에 샘플에 사용한 이미지는 알파가 있습니다.

 


우측의 흑백 이미지가 알파 성분만 표현한 이미지 입니다.
당연히 검은 부분은 알파 성분이 0 이기 때문에
해당 영역은 화면에 표현되지 않을 것입니다.

이번에 살펴볼 샘플은 아래와 같습니다.



똑같은 이미지 파일에서 데이터를 읽었지만,
위의 그림은 알파 처리가 되어서 동물 부분만 출력이 되었습니다.
하지만 아래 부분은 알파처리가 이루어지지 않아서 이미지 영역이 모두 출력되었습니다.

이번 결과의 차이는 WIC를 이용한 것입니다.
우리가 지금껏 무심코(?) 지나쳤던 WIC의 컨버터를 기억하십니까?


제가 이번 샘플을 위해서 약간 개량을 했습니다.
두 사용법에 별 차이는 없지만, GUID_WICPixelFormat... 부분이 보일 것입니다.
이 인자가 바로 두 이미지의 차이를 만들어낸 부분입니다.
즉, 컨버터에서 데이터를 어떤 포맷으로 읽어들일지를 설정하는 부분입니다.

첫번째는 알파처리를 수행하는 방법으로 데이터를 읽어들입니다.
GUID_WICPixelFormat32bppPBGRA는
4개의 색상 채널을 가지고 채널당 8개의 비트를 가지고 있으며,
픽셀 당 32비트를 표현하며 UINT로 각 색상이 저장되어 있는 포맷을 의미합니다.

두번째 알파처리를 하지 않는 GUID_WICPixelFormat32bppBGR은
3개의 색상 채널과 8비트로 각 채널을 표현하는 32비트 픽셀 포맷을 의미합니다.
이 포맷도 UINT로 각 색상 성분을 표현합니다.
이 경우에는 알파 채널이 존재하지만, 실제로 읽어들이지 않습니다.
즉, 알파 채널을 무시하는 것입니다.

관련 포맷이 매우 방대하기 때문에, 여기서 더 이상 자세히 다루지 않습니다.
중요한 것은 이 컨버터 덕분에 별다른 수고 없이 알파 처리를 쉽게 수행할 수 있습니다.

Direct2D 가 사실 많은 부분을 우리가 모르게 자동적으로 처리하는 부분이 많이 있습니다.
Direct2D에서는 프로퍼티를 생성하는 부분들이 많이 있습니다.
예를 들면 다음과 같은 것들입니다.



사실 이 프로퍼티 정보들을 별도로 설정하지 않으면,
자동적으로 Direct2D에서 처리를 해버립니다.
혹은 디폴트 생성자들이 모두 들어있습니다.
자동적으로 처리되는 부분들 이외의 기능이 필요하다면,
이들을 잘 제어해야만 하겠지요..^^

부족하지만 샘플을 참조해 드립니다..^^

신고

[StartD2D-5] Direct2D의 리소스 기본 개념.

DirectX 11 2011.07.11 08:30 Posted by 조진현

 

 

Direct2D 리소스( Resource ) 기본 개념

 

Direct2D 에서 리소스라는 것은 단지 메모리를 의미합니다.

그 메모리는 지오메트리(Geometry)나 비트맵 이미지가 될 수도 있습니다.

즉, 이들 리소스는 메모리들이 추상화 된 일종의 개념적인 분류들입니다.

 

이들 리소스를 처리해서 모니터에 최종적으로 보여주는 것이 Direct2D의 역할입니다.

이전 까지 사용되던 GDI 도 바로 이런 역할을 하는 것입니다.

 

하지만, GDI와 Direct2D 는 리소스 처리 방식이 완전히 다릅니다.

앞선 시간을 통해서, GDI는 CPU만을 활용한다고 꾸준히 언급했습니다.

 

반면에, Direct2D는 CPU와 GPU를 동시에 활용합니다.

CPU와 GPU를 동시에 활용하기 때문에,

리소스들도 이들에 맞게 추상적으로 재분류 되어야 합니다.

 

Direct2D는 이 리소스들을 크게 두 가지 형태로 분류합니다.

바로 디바이스 독립적인 리소스( device-independent resource )와

디바이스 의존적인 리소스( device-dependent resource )로 분류하고 있습니다.

이들 두 리소스들은 모두 ID2D1Resource 인터페이스를 상속받습니다.

 

 

디바이스 독립적인 리소스

 

디바이스 독립적인 리소스라고 하는 것은 CPU에 의해서 처리되는 리소스를 의미합니다.

단어 자체에서 볼 수 있듯이,

이들 리소스는 사용자의 그래픽 하드웨어와 상관없는 처리 결과를 보장합니다.

 

뒤에서 살펴보겠지만, 지오메트리를 표현하는 인터페이스들이 여기에 속합니다.

( 조금 더 정확히 나열하면, ID2D1DrawingStateBlock, ID2D1Factory, ID2D1Geometry,

ID2D1GeometrySink, ID2D1SimplifiedGeometrySink, ID2D1StrokeStyle 등이 여기에 속합니다. )

아래는 이런 디바이스 독립적인 리소스들의 계층구조를 보여주는 그림입니다.

 

 

 

 

 

디바이스 의존적인 리소스

 

반면에 디바이스 의존적인 리소스는 GPU에 의해서 처리되는 리소스를 의미합니다.

사용자들이 가지고 있는 그래픽 하드웨어들은 매우 다양하기 때문에, 그 기능들을 모두 동일한 방식으로 처리하는 것이 쉽지 않습니다.

( 각각의 하드웨어마다 성능이나 명령어들이 모두 차이가 나기 때문이겠죠..^^ )

즉, 이들은 사용자의 하드웨어들에 따라서 결과가 차이가 날 수 있습니다.

이 차이라는 것은 눈으로 확인 가능한 차이를 얘기하는 것이 아니라,

그래픽 하드웨어들이 생성하는 명령어들과 성능 등과 같은 차이를 얘기하는 것입니다.

 

우리가 보는 모니터는 그래픽 하드웨어들이 생성한 명령어들의 최종적인 결과입니다.

리소스들도 바로 이 그래픽 하드웨어의 영향을 받아서 결과가 생성되는 것입니다.

 

앞서 우리가 화면에 렌더링 하기 위해서 만들었던 렌더타겟이나 브러시, 이미지 같은 리소스들은 모두 여기에 속합니다.

아래의 그림은 디바이스에 의존적인 리소스들의 계층구조를 표현하는 그림입니다.

 

 

 

 

Direct2D는 리소스 처리 방식은 되도록이면 GPU를 활용합니다.

GPU 처리가 불가능하다면, 자동적으로 CPU로 처리하게 됩니다.

GPU를 활용하는 리소스라면, 더욱 높은 품질로 빠르게 처리할 수 있습니다.

 

이들 리소스에 대한 설명은 다음 시간부터 차근히 살펴보겠습니다.^^

 

신고

이번 시간에는 지난 시간들까지 언급한 내용을 기반으로 해서,
간단한 테셀레이션 작업을 구현해 보려 합니다.

당연한 얘기이겠지만,
하드웨어 기반의 테셀레이션은 하드웨어의 지원이 없으면 매우 느립니다.
즉 DirectX11 이상을 지원하는 그래픽 카드가 아니면,
효과를 눈으로 확인하는 것조차 무척 고통스럽습니다.

그래서 이번 시간에 만들 테셀레이션은 간단히 삼각형 하나를 이용합니다.
우리는 이 삼각형 하나를 가지고 테셀레이션 작업을 수행할 것이며,
DirectX11 을 지원하지 않는 그래픽카드라면
강제적으로 REF 모드로 테셀레이션 작업을 수행하도록 합니다.

먼저 결과 샘플을 보면 아래와 같습니다.



이제 우리가 만들려는 그림이 그려졌으니, 직접 코딩 작업을 시작하겠습니다.
이 글에서는 DirectX11 의 기본 셋팅과 관련한 사항은 생략합니다..^^
자세한 API 적인 설명은 생략을 하니 DirectX 2010 6월 버전의 SDK 의 튜토리얼을 참고하시거나,
'알코코더의 DirectX11' 을 참고하시기 바랍니다.^^

우리가 이번 샘플에서 사용할 버텍스 데이터의 형식은 위치 정보만 있으면 됩니다.
이번 샘플에서는 최대한 간단하게 작성하는 것을 목적으로 했기 때문에,
많은 정보를 필요로 하지는 않습니다..^^
그래서 아래와 같이 간단한 버텍스 형식을 정의했습니다..^^



생소한 데이터 타입이 보입니다. 바로 XMFLOAT3 입니다.
DirectX11 부터는 D3DX 계열의 수학 데이터 타입들은 더 이상 업데이트 되지 않습니다.
지금부터는 XNA Math 라는 수학 라이브러리를 사용합니다.
그렇다고 더 이상 D3DX 계열의 수학 데이터 타입들을 사용할 수 없는 것은 아니니, 안심하시기 바랍니다.
이들에 대해서는 향후 언급할 기회가 있으니,
지금은 D3DX 계열의 수학 클래스 대신에 XNA Math 라는
새로운 수학 클래스를 사용한다는 정도로만 인식하고 넘어가겠습니다.^^


아래는 우리가 애플리케이션 전역으로 사용할 변수들의 선언입니다.



그 동안의 DirectX11을 언급하면서 꾸준히 언급되던 내용이기에 자세한 설명은 생략하겠습니다.

특이할 만한 것이라면, 래스터라이져 스테이트 오브젝트를 2개 만드는 것입니다.
이는 우리의 샘플이 솔리드( Solid ) 한 렌더링과 와이어프레임( Wire-Frame ) 기반의 렌더링으로
전환이 가능하기 때문입니다.

다음은 상수버퍼( ConstantBuffer ) 에 관한 전역 선언들 입니다.



우리는 월드 좌표계의 정점을 버퍼에 입력할 것입니다.
그래서 View-Projection 행렬만 변환을 위해서 필요합니다.
그리고 얼마나 테셀레이션 작업을 세밀하게 할지를 결정하는 상수를 하나 추가합니다.



쉐이더를 컴파일 해주는 보조 함수를 다음과 같이 하나 만듭니다.


이제 본격적으로 시작을 합니다.
InitD3D() 에 각종 초기화 작업을 수행합니다.
앞서 잠깐 언급드렸듯이,
DirectX11을 지원하는 하드웨어가 아니면, 강제로 REF 모드로 동작하도록 합니다.
또한 이 함수에서는 각 쉐이더 스테이지에 대응되는 HLSL 코드를 컴파일 해줍니다.
그리고 이들에 대한 각 오브젝트를 만듭니다.
초기화 작업은 주로 반복적인 작업이 많기 때문에, 설명은 생략합니다.

InitD3D() 에 버텍스버퍼의 데이터를 설정해 줘야 합니다.
이번 샘플에서는 월드 좌표로 정의된 삼각형을 사용할 것입니다.
또한 카메라 공간에 대한 설정도 같이 해 줍니다.
이들에 대한 코드는 아래와 같습니다.


이 정도로 초기화와 관련된 작업을 마무리 합니다.
이제는 프레임 관련한 처리를 작성합니다.( Render() )

이 Render() 부분에서는 상수버퍼에 설정할 데이터들을 다음과 같이 업데이트 합니다.

 


우리는 와이어프레임 모드와 솔리드 모드의 렌더링 방식 둘 다를 표현할 것이기에,
이들에 대한 설정도 아래와 같이 고려해 주어야 합니다.



그리고 마지막으로 입력되는 버텍스 형식을 알려주고 버텍스 버퍼를 연결한 후에,
그리기 작업을 수행합니다.^^



이제 키보드 이벤트에 따라 약간의 변화를 주는 작업을 합니다.
현재는 'w' 키로 렌더링 모드를 Wire 와 Solid 간의 토글이 되도록 설정합니다.
그리고 위/아래 방향키로 테셀레이션의 분할 정도를 증감합니다.

이번 작업은 여기까지 입니다.
지금까지 DX11을 살펴보면서, 언급된 내용들이 대부분이라 전체적으로 설명드리지는 않습니다.
( HLSL 코드도 최대한 간결하게 작성했습니다..^^ )
샘플을 같이 첨부드리니, 직접 작성하시면서 익혀보시기 바랍니다.^^

신고


이제 테셀레이션 작업의 마지막 단계입니다.
테셀레이터를 통한 결과와 Hull Shader 단계에서의 결과인 패치 데이터가
Domain Shader 의 입력으로 전달
이 되게 됩니다.

Domain Shader 에서는
우리가 DX9 세대에서 주로 수행했던 Vertex 변환 작업을 수행하게 됩니다.
즉, 실제적으로 Projection 변환까지 Domain Shader 단계에서 이루어지게 됩니다.


테셀레이션 작업만으로는 폴리곤의 퀄리티를 향상시키는데에 효과적이지 못합니다.
그래서 실제적으로 높이 정보를 포함하고 있는 텍스쳐를 사용하게 되는데,
이 텍스쳐를 사용하는 것을 'Displacement mapping' 이라고 합니다.

아래의 그림을 살펴보도록 하겠습니다.




첫번째 모델은 로우 폴리곤으로 제작된 것입니다.
두번째가 바로 테셀레이션 작업이 종료된 폴리곤 모델입니다.
( 아주 미끄러운 캐러멜 같은 느낌이지요? ^^ )

세번째는 테셀레이션 작업을 마치고, Displacement mapping 까지 마친 모델입니다.
Displacement mapping 의 특징은 실제 Vertex 데이터를 조작하는 것에 있습니다.
위의 작업을 단순하게 표현해 보면 아래의 그림과 같습니다.



수년 전부터 가장 많이 쓰이고 있는 Bump mapping 기법은 우리의 눈을 속이기 위한 방법이라면,
Displacement mapping 은 눈속임이 아니라, 실제로 데이터를 조작하는 텍스쳐 기법입니다.
아래의 그림은 이들에 대한 차이점을 질감적으로 잘 표현해 주고 있습니다. ^^
Bump mapping의 경우에는 실제로 평면이지만,
텍스쳐링 효과를 이용해서 우리에게 질감의 느낌을 전달해 주고 있습니다.
반면에 Displacement mapping은 실제 Vertex 데이터를 조작해서 질감의 느낌을 전달합니다.



Displacement mapping 의 설명에 제가 열을 올리는 이유는
바로 이 작업 Domain Shader 단계에서 계산되어서 적용되기 때문입니다.
앞서 설명했듯이, Displacement mapping 은 실제 Vertex 를 조작하는 기법이기 때문에
최종 Vertex 를 생성하는 Domain Shader 에서 적용되는 것이 당연한 일입니다.^^
이 작업 자체가 어려운 일은 당연히 아닙니다.
하지만, Displacement mapping을 고려하게 됨으로써,
Vertex 변환 단계가 조금 복잡해 진 것은 사실입니다.
텍스쳐링의 단계를 수행하고, 그 텍셀의 수치만큼 실제 Vetex를 조정해 주어야 하기 때문입니다.
거기다, 쉐이더 단계에서 LOD 레벨을 고려해야하기 때문에
쉐이더의 Texture 멤버함수로 SampleLevel() 를 이용하는 상황이 생기기도 할 것입니다.
또 하나의 더 큰 효과를 위한 하나의 방법이 늘었다고 생각하시면 더 좋을 것 같습니다.^^

신고

[StartD2D-1] Good-bye~~ GDI…

DirectX 11 2011.03.17 08:00 Posted by 조진현

 

'윈도우즈 7 운영체제가 왜 좋은 것일까?'

 

이 물음에 프로그래머라면, 어떤 대답을 할 수 있을까요?

여러 가지 대답들이 존재하겠지만, 그 중에 하나인 부분을 지금부터 진행하려 합니다.

지금부터 언급하는 내용은 모니터에 무엇인가를 표현하는 것과 관련이 있습니다.

즉, 드로잉( Drawing ) 작업에 관한 내용입니다.

 

혹시 여러분들은 'GDI' 와 'DirectX' 이들 단어에 대해서 들어본 적이 있습니까?

컴퓨터를 가지고 작업을 경험했던 이들이라면, 들어봤을 법한 용어들입니다.

 

여러분들이 사용하는 컴퓨터의 모니터는 어떤 과정을 거쳐서 우리에게 전달되는 것일까요?

분명히 이것은 하드웨어적인 영역일 수도 있습니다.

조금 더 애플리케이션 프로그래머 관점으로 이 호기심을 확장해 보겠습니다.

'어떻게 하면 모니터 화면을 제어할 수 있을까?'

'어떻게 하면 내가 원하는 모습으로 모니터의 화면에 나타낼 수 있을까?'

'윈도우즈 운영체제에서 화면을 무엇인가를 그리는 시스템은 어떻게 구성되어 있을까?'

 

 

위의 그림은 XP를 포함한 이전 세대의 Windows 운영체제( 이하 XP세대 )가

화면을 제어하는 시스템 구조입니다.

즉, 하드웨어 추상화 계층이라고도 합니다.

XP세대의 운영체제에서는 화면에 무엇인가를 그리기 위해서는

GDI 와 DirectX API, 이 두 가지를 통해서 제어가 가능했습니다.

최종적으로 GDI 와 DirectX API 는 드라이버와 통신을 하면서,

우리의 명령어를 처리해서, 하드웨어로 결과를 보내주게 됩니다.

 

그런데 왜 동일한 일을 하는 구조가 2개씩이나 존재하는 것일까요?

그것은 컴퓨터의 탄생과 발전이 예상을 벗어났기 때문에 생긴 일입니다.

초창기 컴퓨터의 구조는 굉장히 간단했습니다.

아래의 그림을 살펴보겠습니다.

 

 

컴퓨터라는 것은 결국 CPU 에게 연산을 부탁해서 처리된 결과를 메모리에 저장을 합니다.

그리고 이들에 대한 제어를 우리는 I/O 장치들을 통해서 수행하고 확인합니다.

초창기 컴퓨터는 프로세싱 유닛이 CPU 오직 하나였습니다.

그래서 API들은 프로세싱 유닛이 CPU 하나임을 전제로 개발되었습니다.

우리가 지금 배우고 있는 Windows API 역시 CPU가 한나인 경우에

포커스를 두어서 개발되었던 API 였습니다.

 

하지만 오늘날의 컴퓨터는 어떠합니까?

컴퓨터에서 CPU의 수는 2개 이상이고, 앞으로도 그 개수는 증가할 것입니다.

거기다 이제는 프로세싱 유닛이 CPU 뿐만 아니라, GPU 도 활용이 가능한 시대입니다.

메모리는 또 얼마나 발전하고 있습니까?

굉장히 방대한 용량의 메모리를 여러 개 꽂아서 사용하고 있으며,

그래픽 카드에는 비디오 메모리라는 것도 별도로 존재합니다.

컴퓨터의 큰 구조는 변한 것이 없지만, 기능이 점점 세분화 되고 있는 것입니다.

 

이런 하드웨어들의 변화들은 결국 API를 만드는 사람들의 개발 패러다임을 변화시킵니다.

그러한 패러다임의 변화는 애플리케이션 개발자들에게 새로운 학습을 요구합니다.

즉, 여러분들은 그런 변화에 맞추어서 더 많은 공부를 해야 하는 것입니다.T.T

 

 

초창기 Windows 운영체제 차원에서 화면에 그리기 위한 방법은,

GDI 관련 API 를 사용하는 것뿐 이였습니다.

운영체제 차원에서 모든 것을 관리하고 싶었기 때문에,

MS측에서도 별다른 문제를 느끼지 못했을 것입니다.

그런데 예측을 빗나간 부분이 있었는데, 그것은 게임 이였습니다.

게임 개발자들은 GDI 라는 훌륭한 모델이 있음에도 불구하고,

여전히 MS-DOS 기반으로 화면을 제어하고 있었던 것 이였습니다.

이유는 간단했습니다.

GDI 를 활용하면 분명히 쉬운 방법으로 개발을 할 수 있었지만,

DOS 기반의 방법보다 상대적으로 느렸습니다.

MS-DOS를 활용하면 하드웨어를 개발자가 직접 제어할 수 있기 때문에,

Windows 운영체제의 관리를 받는 GDI 보다는 훨씬 빨랐던 것입니다.

( 이 당시만 해도, CPU 사이클을 줄이는 것이 큰 최적화 이슈였습니다. )

MS-DOS를 빨리 벗어나려 했던 MS 입장에서는 적지 않은 충격(?)을 받게 됩니다.

물론 이런 문제는 게임뿐만 아니라,

멀티미디어 관련 개발에서도 나타났던 문제였습니다.^^

 

그렇게 해서 등장한 대한이 바로 DirectX 입니다.

처음 명칭이 GameSDK 였음에서 알 수 있듯이 게임 개발자를 위한 API 였습니다.

이 DirectX는 Windows 95 운영체제부터 추가되어서 활용되기 시작했습니다.

어쩌면, GDI 모델을 완전히 수정할 여유나 계획은 없었을지도 모릅니다.

어찌되었든 현재까지 GDI 와 DirectX는 이렇게 공존하면서 XP 운영체제까지 이르게 되었습니다.

 

XP 세대까지는 화면에 무엇인가를 그리는 작업을 기준으로 봤을 때는,

애플리케이션은 DirectX를 활용하는 애플리케이션과 GDI를 활용하는 애플리케이션으로 나눌 수 있었습니다.

 

GDI( Graphics Device Interface ) 라는 용어에서 보듯이,

일반적인 윈도우 애플리케이션에서 모니터 화면에 무엇인가를 출력하기 위해서는

GDI 라는 것을 이용해서 구현합니다.

이는 2D API 라고 볼 수 있습니다.

GDI를 통해서 애플리케이션과 디바이스 드라이버( Device driver ) 사이를
제어
할 수 있습니다.

XP 세대에서 화면에 보여지는 대부분의 구성은 바로 이 GDI 를 활용한 것입니다.

GDI는 소프트웨어적으로 처리되기 때문에,

그래픽 하드웨어와 상관없이 처리할 수 있다는 큰 장점이 있습니다.

즉, CPU가 그래픽 처리에 필요한 연산을 수행합니다.

 

일반적인 윈도우 애플리케이션은 그래픽 하드웨어를 직접적으로 접근( access )할 수 없습니다.

반드시 GDI를 통해서 디바이스 드라이버에 접근해야 합니다.

GDI 의 최대 강점은 바로 디바이스 독립적( device-dependent ) 이라는 것입니다.

세상의 모든 PC가 동일한 사양을 갖추고 있으면 좋겠지만,

PC들은 저마다 다른 성능과 사양을 갖추고 있기 때문에,

이를 모두 제어하는 것은 굉장히 어렵고 힘든 일입니다.

하지만, GDI 코드는 어떠한 장치에 상관없이 화면에 공통된 그래픽을 표현할 수 있습니다.

왜냐하면 CPU가 이들에 대한 처리를 모두 수행하기 때문입니다.

GDI는 Windows 운영체제의 매우 오래된 기술 중 하나이며,

현재까지도 가장 널리 이용되는 윈도우 그래픽 API 라고 할 수 있습니다.

하지만 최근의 멀티코어 환경과 GPU의 발전은 많은 변화들을 가져왔으며,

GDI도 이러한 변화에 예외가 될 수 없었습니다.

급기야 이제는 DirectX가 GDI를 서서히 대체해 나가는 방향으로
가는 길이 정해지고 말았습니다.T.T

Good-bye!! GDI~~

신고



DirectX11의 파이프라인은 앞선 시간에서 우리는 꾸준히 보았습니다.
복습의 의미에서 이번에는 http://www.realtimerendering.com 에 정의된 Direct3D 의 파이프라인입니다.

Direct3D 10 Pipeline
< Direct3D 10 pipline >

Direct3D 11 Pipeline
< Direct3D 11 pipline >

우리가 그래픽스에서 사용하는 폴리곤은 굉장히 복잡한 방식으로 처리가 됩니다.
많은 스테이지를 통해서 결국 우리는 화면 픽셀로 변환된 최종 결과를 확인하게 되는 것입니다.
그 과정 속에서 Direct3D 9에서는 Vertex와 Pixel 을 조작할 수 있도록 변화되어 왔습니다.
Direct3D 10 은 여기에 Geometry 까지 조작할 수 있도록 프로그래머들에게 개방되었습니다.
Direct3D 11 은 무려 3개의 스테이지가 추가되었습니다.
Hull Shader, Tessellator, Domain Shader 가 바로 그것들입니다.

이 중에 프로그래머가 제어하는 부분은 Hull / Domain Shader 이며,
Tessellator 의 경우에는 하드웨어가 직접 처리하게 됩니다.

테셀레이션을 언급하면서 가장 많이 나오는 주제는 현재 LOD( Level of Detail ) 처리 이지만,
정확하게 테셀레이션이 필요한 이유는 글인 http://vsts2010.net/331 을 통해서 확인할 수 있습니다.

현재 그래픽 파이프라인에서 테셀레이션 작업은 현재 옵션으로 설정되어 있습니다.
여러분이 이 기능을 사용하기 원하지 않는다면, 이들을 활성화 시키지 않으시면 됩니다.
그렇게 된다면, 기존의 파이프라인과 동일한 방식으로 Vertex 데이터를 처리하게 됩니다.


< Hull Shader >

Hull Shader 는 테셀레이션 작업의 시작입니다.
하지만, 실제로 프로그래머의 시작은 Vertex Shader 입니다.
DirectX9에서 VertexShader 는 World-View-Projection 변환을 수행하는 것이 가장 큰 목적이였습니다.
DirectX11에서 VertexShader 의 목적은 Hull Shader 로의 데이터를 전달하는 것입니다.
즉, 테셀레이션이 목적인 경우에는
DirectX11에서 VertexShader 스테이지에서 World-View-Projection 을 수행해서는 안됩니다.
테셀레이션 작업시 VertexShader 에서 처리되는 Vertex는 실제 우리가 사용하는 데이터가 아닙니다.
우리는 VertexShader 의 입력으로 들어오는 데이터를 모아서,
많은 수의 Vertex를 새롭게 생성시켜야 합니다.
그래서 테셀레이션 작업시 VertexShader 스테이지에서는 Vertex를 월드 변환까지만 수행합니다.

Hull Shader 에서는 '폴리곤을 어떻게 분할할 것인가?' 와 '폴리곤을 얼마나 분할할 것인가?' 를 결정합니다.
가장 단순한 형태로 이 Hul Shader의 기능을 표현하면 다음과 같습니다.

Diagram of the hull-shader stage

위의 그림은 MSDN 의 그림입니다.

Hull Shader 는 두 가지의 작업을 동시에 수행합니다.
그것은 제어점( Control Point ) 를 생성하는 작업과 Patch Constant Data 를 계산하는 작업입니다.
이들 작업은 병렬적으로 수행되게 됩니다.
HLSL 코드는 실제로 드라이버 수준의 하드웨어 명령어를 생성하게 되는데,
이 때, 병렬처리가 가능한 형태로 변환되게 됩니다.
이는 Hull Shader 가 빠르게 동작할 수 있는 중요한 이유이기도 합니다. 
 
Hull Shader 의 입력으로 들어오는 제어점( Control Point )들은
낮은 차수의 면을 표현하는 정점들입니다.
이를 높은 차수의 면을 표현하는 제어점들로 만들어 내게 됩니다.
이 때 생성된 제어점들은 Tessellator 스테이지에서 사용되는 것이 아니라,
그 다음 스테이지인 Domain Shader 에서 사용됩니다.



위의 그림은 베지어(Bezier) 제어점들을 이용해서 베지어 곡면을 표현한 것입니다.

근본적으로 테셀레이션은 평면을 곡면으로 생성시키는 개념과 매우 비슷합니다.
( 굳이 평면을 많은 갯수의 폴리곤으로 표현할 필요는 없기 때문이겠죠. )
그렇기 때문에, 분할 방법으로 사용되는 알고리즘들은 베지어처럼 게임 프로그래머들에게 친숙한
개념들이 사용됩니다.

Hull Shader 의 또 하나의 중요한 역활은 불필요한 연산을 줄이기 위해
테셀레이션 단계를 스킵할지를 결정할 수 있다는 것입니다.
즉, Hull Shader 에서 Tessellation Factor 가 0 이하인 경우에
이 패치는 컬링
되어 버린 것으로 간주됩니다.
( Tessellation Factor 는 얼마나 분할할지를 나타내는 수치적 비율입니다. )
이로인해 더 이상 파이프라인 처리가 이루어지지 않음으로써,
성능 향상을 도모할 수 있습니다.
( 폴리곤을 처리하지 않는 것이 가장 큰 성능의 이득이겠죠..^^ )


그러면 과연 Hull Shader 에서의 '폴리곤을 어떻게 분할할 것인가?' 와 '폴리곤을 얼마나 분할할 것인가?'
프로그램 코드에서는 어떻게 표현해야 할까요?

현재 MSDN 에 나와있는 Hull Shader 의 가장 단순한 형태는 다음과 같습니다.
( 물론 실제로 구현되고 동작되는 내용들의 예들은 DirectX11 샘플에 있습니다. )


[domain("quad")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(16)]
[patchconstantfunc("SubDToBezierConstantsHS")]
BEZIER_CONTROL_POINT MainHS( InputPatch<VS_CONTROL_POINT_OUTPUT, MAX_POINTS> ip, 
                                                    uint i : SV_OutputControlPointID,  uint PatchID : SV_PrimitiveID )
{
    VS_CONTROL_POINT_OUTPUT Output;

    // Insert code to compute Output here.    
    return Output;
}

위의 Hull Shader 는 동작 방식을 설정합니다.
몇몇 정의된 값들을 셋팅해 주면, 이는 테셀레이션 작업을 하는 동안에 사용되게 됩니다.
즉, 위의 셋팅들은 '폴리곤을 어떻게 분할할것인가?' 에 준하는 프로그램 코드라 할 수 있습니다.

이제 남은 것은 '폴리곤을 얼마나 분할할 것인가?' 입니다.
이는 PatchConstantFunc 을 통해서 병렬적으로 처리된다고 앞서 설명을 했습니다.
이곳에서는 Tessellation Factor 를 계산하게 되는데, 그 결과에 따라서 컬링 작업이 실행됩니다.
( 이 값이 0 이하의 경우에는 더 이상 처리가 필요하지 않습니다. )
이 작업을 하는 함수를 우리는 직접 작성해서,
 위의 [patchconstantfunc("SubDToBezierConstantsHS")] 처럼 설정해 주면 자동적으로 동작합니다.
MSDN 에 나와있는 PatchConstantFunc의 기본적인 형태는 다음과 같습니다.

#define MAX_POINTS 32

// Patch Constant Function
HS_CONSTANT_DATA_OUTPUT
SubDToBezierConstantsHS( InputPatch<VS_CONTROL_POINT_OUTPUT, MAX_POINTS> ip,
                                         uint PatchID : SV_PrimitiveID )

    HS_CONSTANT_DATA_OUTPUT Output;

    // Insert code to compute Output here    
    return Output;
}

이 PatchConstantFunc 의 결과에 바로 '폴리곤을 얼마나 세밀하게 분할할 것인가?' 에 대한 정보들이 있습니다.

// Output patch constant data.
struct HS_CONSTANT_DATA_OUTPUT
{
    float Edges[4]        : SV_TessFactor;
    float Inside[2]       : SV_InsideTessFactor;
    ...
};

위의 경우의 결과 구조체는 사각형을 분할한 경우이며,
우리가 주로 사용하는 삼각형 분할의 경우에는 다음과 같을 것입니다.

// Output patch constant data.
struct HS_CONSTANT_DATA_OUTPUT
{
    float Edges[3]        : SV_TessFactor;
    float Inside       : SV_InsideTessFactor;
    ...
};

지금까지 Hull Shader의 기본적인 개념과 역활에 대해서 언급해 드렸습니다.
이렇게 얻어진 결과는 테셀레이터로 전달되게 됩니다.
세부적인 Hull Shader 의 작성은 이후의 시간들을 통해서 살펴볼 예정입니다.
( 현재 본 글들은, 개념 위주의 설명에 포커스를 두고 있습니다. ^^ )

신고


앞선 시간을 통해서 ID3DXPatchMesh 를 이용하면
간단하게 테셀레이션이 적용된 메시를 만들 수 있음을 언급했었습니다.
실제로 D3DX 유틸리티 클래스들이 테셀레이션을 손쉽게 적용할 수 있도록 구비가 되어있습니다.
그렇다는 것은 실제로는 DirectX 내부적으로 코어한 API가 있다는 얘기입니다.

테셀레이션과 관련한 DirectX 에서 코어한 API가 바로
IDirect3DDevice9::DrawTriPatch() 와 IDirect3DDevice9::DrawRectPatch() 입니다.
API 이름에서 쉽게 이해할 수 있듯이 전자는 삼각형과 관련한 것이고 후자는 사각형과 관련한 것입니다.
두 함수의 원형은 다음과 같습니다.

HRESULT DrawTriPatch
(
  [in]  UINT Handle,
  [in]  const float *pNumSegs,
  [in]  const D3DTRIPATCH_INFO *pTriPatchInfo
);


HRESULT DrawRectPatch(
  [in]  UINT Handle,
  [in]  const float *pNumSegs,
  [in]  const D3DRECTPATCH_INFO *pRectPatchInfo
);


그런데 조금 생소한 구조체 정보를 함수 인자로 받습니다.
이 두 API들은 함수 이름에서도 알 수 있듯이 실제로 렌더링을 수행하는 API 입니다.
테셀레이션을 위해서는 테셀레이션을 위한 정보들이 존재해야 합니다.
이들에 대한 설정 작업이 이루어져야 하는데,
이를 위한 구조체가 세번째 인자인 D3DTRIPATCH_INFO와 D3DRECTPATCH_INFO 입니다.
사각형과 관련한 작업은 삼각형과 유사하기 때문에 지금부터는 삼각형에 국한에서 글을 진행하겠습니다.


D3DTRIPATCH 구조체의 원형은 다음과 같습니다.

typedef struct D3DTRIPATCH_INFO
{
  UINT          StartVertexOffset;
  UINT          NumVertices;
  D3DBASISTYPE  Basis;
  D3DDEGREETYPE Degree;
} D3DTRIPATCH_INFO, *LPD3DTRIPATCH_INFO;


이 구조체는 버텍스 버퍼처럼 오프셋과 버텍스 갯수를 먼저 설정합니다.
D3DBASISTYPE 은 고차원 패치( high-order patch )의 기본 타입을 설정합니다.
삼각형의 경우에는 D3DBASIS_BEZIER 만 설정할 수 있습니다.

D3DDEGREETYPE 는 고차원 패치의 차수 정도를 설정하게 됩니다.
즉, 곡선을 표현하는 방정식의 차수를 표현하는데,
높은 차수를 선택할 수록 당연히 연산량이 많아질 것입니다.

이들에 대한 종류는 다음과 같습니다.

종류

버텍스 갯수
D3DDEGREE_CUBIC 10 ( 3차 방정식 )
D3DDEGREE_LINEAR 3   ( 1차 방정식 )
D3DDEGREE_QUADRATIC N/A ( 지원되지 않음 ) ( 2차 방정식 )
D3DDEGREE_QUINTIC 21 ( 4차 방정식 )


아래의 그림은 Cubic Bézier 방식의 삼각형 패치를 보여주고 있습니다.


Diagram of a triangular high-order patch with nine vertices


중간 중간에 생성된 정점을 기준으로 테셀레이션 작업이 수행될 것입니다.^^
이런 테셀레이션과 관련한 API들이 DirectX9 에 있었지만, 사실 거의 사용되지는 못했습니다.
왜냐하면, 정말이지 많은 연산을 필요로 하기 때문이겠죠? ^^
즉, DirectX9의 테셀레이션 작업은 소프트웨어적으로 에뮬레이션 되는 테셀레이션입니다.
신고


이번 시간에는 간단하게 페이지 간의 전환을 해보려고 합니다.
그러기 위해서는 최소 2개 이상의 페이지가 존재해야 합니다.
그래서 첫 작업은 페이지를 추가하는 작업입니다.
역시나 이번에도 VisualStudio가 아닌, 블렌드를 실행합니다.

그리고 프로젝트에서 아래 그림과 같이 새로운 아이템을 추가합니다.








이렇게 하면 새로운 페이지가 추가되어져 있을 것입니다.
그리고 첫번째 페이지에 간단히 버튼을 추가합니다.
버튼은 Asset 패널에 컨트롤 메뉴에 있습니다.
버튼을 배치하면, 각종 속성을 제어할 수 있도록 화면 우측에 여러 윈도우가 활성화 되는 것을 볼 수 있습니다.





버튼만 화면에 있으니깐 너무 단순한 것 같아, 이미지를 화면에 배치시키겠습니다.
이미지를 위해서 프로젝트 내부에 폴더를 만듭니다.



그렇게 폴더가 만들어진 후에는 'Add Existing Item...' 메뉴를 통해서 이미지를 추가합니다.



이 때, 이미지 파일 어느 곳에 있든 상관이 없습니다.
왜냐하면, 현재 작업중이 프로젝트로 이미지 파일을 새롭게 복사를 해오기 때문입니다.
현재 작업 중인 프로젝트를 열어서 확인해 보시기 바랍니다.
이미지 파일이 자동적으로 생성되어서 복사되었음을 확인할 수 있을 것입니다.

이제 이미지를 배치해 보겠습니다.
이미지 역시 컨트롤이기 때문에 Assets 탭에서 Image 컨트롤을 선택해서 적당히 배치하시기 바랍니다.
같은 방법으로 두번째 페이지에도 추가해 주시면, 결과 확인이 쉽겠죠? ^^



이미지의 경우에는 컨트롤만 연결되었지, 아무런 변화가 없을 것입니다.
즉, 이미지 리소스를 연결해 주어야 합니다.
아래 그림처럼 현재 연결 가능한 리소스 리스트에서 하나를 선택해서 연결해 주시면 됩니다.




각 페이지마다 이미지가 잘 등장했으리라 예상이 됩니다.

혹시나 컨트롤이 잘 선택이 안되어지는 경우에는 아래와 같은 패널을 이용하시기 바랍니다.
이 패널은 컨트롤을 가장 확실하게 선택할 수 있습니다..^^





이제는 속성 창에 대한 설명을 뒤로 한채 간단하게 저 버튼들에 이벤트를 연결해 보겠습니다.
아래 그림에서 빨간색 타원 영역이 보이십니까?
그 영역에서 번개 모양의 아이콘을 클릭하면 이벤트를 연결할 수 있습니다.
버튼이 선택되어진 상태에서 해야, 버튼에 관련한 이벤트를 연결할 수 있습니다.





번개 모양의 아이콘을 클릭하면 다음과 같은 이벤트 속성 창이 등장합니다.



저는 Click 속성에 Next 를 입력한 후에 엔터키를 쳤습니다.
그러면 Next 라는 이벤트용 함수가 만들어 집니다.
각각의 상황을 모두 제어하고 싶다면,
바로 이렇게 이벤트 속성에서 함수명을 입력하고 제어하면 되는 것입니다.
아래와 같이 친절하게 함수를 만들어 줍니다.^^




페이지 전환을 위해서 저는 아래와 같이 코드를 추가했습니다.



두번째 페이지에서도 동일한 방법으로 페이지 파일명만 교체해서 추가해 주었습니다.
제가 만든 결과는 다음과 같습니다.^^



버튼을 클릭할 때마다, 페이지 이동이 발생합니다.^^
실행 프로젝트는 첨부를 하니, 참고해 주시기 바랍니다.^^
이번 시간에 Uri가 무엇인지, NavigationService 가 무엇인지 등 코드에 대한 설명하지 않습니다.^^
실제 코드 개발 관련 내용은 다른 분들이 다루어 주실 것입니다.



신고


얼마전 윈도우 폰 개발 툴 베타 버전( Windows Phone Developer Tools Beta )이 배포되기 시작했습니다.


http://www.microsoft.com/downloads/details.aspx?FamilyID=c8496c2a-54d9-4b11-9491-a1bfaf32f2e3&displaylang=en


이번 개발 툴 버전의 가장 큰 특징 중 하나는
'Microsoft Expression Blend for Windows Phone Beta'의 포함입니다.
지난 버전까지는 별도의 애드인 작업을 거쳐야 했는데, 이번 버전부터는 포함되어 있습니다.

윈도우폰7 개발에 많은 관심이 있으신 분들 중에는
Expression Blend( 이하 블렌드 ) 라는 소프트웨어에 대해서 굉장히 생소할 수 있습니다.
이 소프트웨어는 마이크로소프트에서 개발되었으며,
실버라이트나 WPF 와 같은 마이크로소프트 플랫폼에서 손쉬운 그래픽 디자인을 위해서 탄생했습니다.
처음 공개적으로 배포된 것이 2007년 1월이니, 정말 최신 소프트웨어라고 할 수 있습니다.
( 현재는 4.0까지 출시되었으며, 이것이 2010년 6월의 일입니다. )

알려진 바와 같이 윈도우폰7 개발 방법은 크게 2가지로 나누어져 있습니다.
실버라이트를 기반으로 한 일반 애플리케이션 분야와
XNA 를 기반으로 한 게임 분야입니다.



아주 불행하게도 XNA와 블렌드는 연동되지 않습니다.
( 이것은 블렌드를 조금이라도 아는 분들이라면, 정말 불행한 일이라고 생각하실 것입니다. )

무슨 말이 더 필요하겠습니까?
지금 당장 윈도우폰 개발 베타 버전을 설치하신 후에 Visual Studio가 아닌,
Microsoft Expression Blend for Windows Phone Beta 실행하시기 바랍니다.




아마 위와 같은 화면이 실행되어졌을 것입니다.
실행 후에는 이 블렌드를 통해서 윈도우폰7 프로젝트를 생성합니다.







적당한 경로에 자신이 원하는 프로젝트 명칭으로 설정해주면 됩니다.
그리고 OK를 클릭하면, 프로젝트가 생성됩니다.




기존의 디자인툴에 익숙하신 분들이라면, 친숙한 모습일 것입니다.
블렌드는 사실 기능이 너무 많습니다.
개인적인 생각이지만, 포토샵 + 플래시 + 드림위버 같은 느낌이였습니다.
여러분들은 어떠신가요? ^^

그러면 코딩은 어디서 해야 할까요?
의구심이 드시겠죠? ^^
실제 코딩은 블렌드 내부에서도 가능하고, Visual Stduio를 별도로 띄워서도 가능합니다.
이것이 제가 Visual Studio 가 아닌, 블렌드를 실행시키라는 진정한 의미입니다.
즉, 개발과 그래픽 디자인이 모두 하나의 툴에서 이루어지는 것입니다.

'Projects' 탭에서 아래 그림처럼 해보시기 바랍니다.
트리 메뉴에서 MainPage.xaml.cs 에서 마우스 오른쪽을 누르시면 됩니다.



위와 같이 실행하면, 별도로 Visual Studio가 실행됩니다.
물론 Visual Studio에서 수정하면, 즉시 블렌드쪽에 결과가 반영이 됩니다.

*.cs 파일을 더블 클릭하면, 블렌드 상에서 바로 수정할 수 있는 창이 실행됩니다. ( 완전 좋다는.... )
*.xaml 파일을 더블 클릭하면, 다시 디자인 폼 수정 상태로 이동할 수 있습니다.



실행은 다음과 같이 수행합니다.








이번 베타버전에서는 빌드 성능도 꽤 많이 좋아졌습니다.
에뮬레이터 모양도 굉장히 예뻐졌습니다..^^


신고


DirectX11 을 통해서 가장 많은 관심을 가지고 있는 부분 중 하나인 테셀레이션( Tessellation )은
갑자기 등장한 새로운 기능이 아닙니다.


< DirectX9에서의 테셀레이션의 등장 >

DirectX9 이 처음 세상에 등장할 때, 아래와 같은 특징들을 나열했었습니다.

- 2-D support
blt, copy, fill operations, GDI dialogs
- Adaptive tessellation
- Displacement mapping
- Two-sided stencil operations
- Scissor test rect
- Vertex stream offset
- Asynchronous notifications
- VS / PS 2.0
Flow control, float pixels
- Multiple render targets
- Gamma correction


Adaptive tessellation 이 보이시죠?
저도 그냥 무심코 지났던 DirectX9 소개 자료에서 우연히 찾았습니다.^^


< Adaptive tessellation >

테셀레이션에는 몇 가지 방법이 있는데,
그 중에 가장 유명한 것이 Adaptive 형식과 Uniform 형식입니다.
아래의 이미지를 보시기 바랍니다.


< 이미지 출처 : GPU Gems 2권 >


좌측의 경우가 Adaptive 한 방식입니다.
Adaptive 한 방식을 간단히 설명드리면,
시점의 위치에 근거에서 얼마나 많은 면을 생성할 지를 판단해서,
테셀레이션 작업
을 하는 것입니다.

반면에 Uniform 한 방식은,
모두 균일한 면의 갯수로 테셀레이션 작업을 수행하는 방법
입니다.
Uniform 한 방식이 더 연산 수가 많은 것이 일반적이기 때문에,
Adaptive 한 방식이 게임 분야에서 주로 사용됩니다.



< 테셀레이션을 위해 필요한 정보 >

테셀레이션 작업을 위해서는 두 가지가 필요합니다.
그것은 제어점들( Control Points )과 테셀레이션 팩터들( Tessellation Factors ) 입니다.
제어점들은 파이프라인에 입력으로 들어감으로써 패치( Patch ) 형태로 변환되어서
최종적으로 렌더링
되게 됩니다.
이 과정에 대한 자세한 설명은 앞으로도 꾸준히 언급될 것입니다.
지금은 간단하게 이 정도로만 설명하고 넘어가겠습니다.^^



< ID3DXPatchMesh >

그러면 DirectX9 은 어떤 방식으로 테셀레이션 작업을 지원했을까요?
그것은 ID3DXPatchMesh 라는 인터페이스를 통해서 간접적으로 지원했습니다.

참고적으로 얘기드리면, DirectX 에서는 D3DX 라는 유틸리티를 통해서
메시를 관리할 수 있는 클래스를 제공했습니다.
ID3DXBaseMesh, ID3DXMesh, ID3DXSPMesh, ID3DXPMesh,
그리고 마지막으로 언급드렸던 ID3DXPatchMesh 입니다.

ID3DXPatchMesh 인터페이스는 다른 메시들을 지원하는 클래스와 다릅니다. 
일반적인 메시 인터페이스들은 ID3DXBaseMesh와 계층 관계를 이루는 반면에,
ID3DXPatchMesh 는 완전히 별도로 구성된 클래스입니다.
즉, ID3DXPatchMesh 클래스는 IUnknown 인테페이스를 상속받습니다.


ID3DXPatchMesh는 테셀레이션 작업을 위해서 각종 멤버 함수를 가지고 있습니다.
실제로 테셀레이션 작업을 하는 함수는 ID3DXPatchMesh::Tessellate() 와
ID3DXPatchMesh::TessellateAdaptive()
입니다.
이들 함수에 대한 형태는 다음과 같습니다.

HRESULT Tessellate
(
  [in]  FLOAT fTessLevel,
  [in]  LPD3DXMESH pMesh
);

HRESULT TessellateAdaptive
(
  [in]  const D3DXVECTOR4 *pTrans,
  [in]  DWORD dwMaxTessLevel,
  [in]  DWORD dwMinTessLevel,
  [in]  LPD3DXMESH pMesh
);

두 멤버함수 모두 LPD3DXMESH 형태의 테셀레이션 작업이 끝난 메시를 리턴합니다.

이들에 대한 모든 작업은 CPU 가 담당합니다.
또한 연산량도 많기 때문에 Adaptive Tessellation을 처리하기는 상당한 무리가 있습니다.
왜냐하면 Adaptive Tessellation은 시점에 근거해서 매번 폴리곤을 생성해야하기 때문입니다.
ID3DXPatchMesh::Optimize() 라는 최적화 함수를 미리 호출해 줄수도 있지만,
그래도 이는 분명 매우 부담스러운 연산입니다.

< 마치면서... >
이상으로 ID3DXPatchMesh 를 활용한 DirectX9 의 테셀레이션 작업에 대해서 살펴보았습니다.
DirectX9 에서의 테셀레이션 작업의 불편함과 성능 문제를 이해한다면,
DirectX11 에서의 테셀레이션 작업의 우수성을 알 수 있을 것이라 생각됩니다.
다음 시간에도 계속 DirectX9 에서의 테셀레이션 작업에 대해서 살펴보겠습니다.^^
신고
- 뜬금없이 뭐여..?

지금까지는 닷넷 4.0에 추가된 TPL과 PLINQ를 통해서 멀티 스레드 프로그래밍을 하는 방법을 살펴봤습니다. 그러면, 잠깐 추억을 되살릴겸, 뭐가 어떻게 달라졌는지도 한번 비교해 볼겸 해서, 닷넷 3.5까지의 멀티 스레드 프로그래밍 방법을 잠깐 살펴보도록 하겠습니다. 호호호호


- Thread와 다이다이로 작업하던 시절.

TPL은 System.Threading.Tasks를 사용해서, ThreadPool을 내부적으로 사용한다고 말씀을 드렸었습니다. 하지만, 그것 닷넷 4.0이나, 닷넷 3.5에서는 Reactive Extension(Rx)을 통해서 추가적으로 지원하는 기능이구요. 그 이전에는 직접적으로 Thread나 ThreadPool을 이용해서 프로그래밍 해야 했습니다. 그럼 Thread를 직접 사용하던 코드를 예제로 한번 보시죠.

using System;
using System.Threading;

namespace Exam18
{
    class Program
    {
        static readonly int max = 10000;

        public static void PrintAsync()
        {
            for (int count = 0; count < max; count++)
            {
                Console.Write("|");
            }
            Console.WriteLine("추가 스레드 끝");
        }

        static void Main(string[] args)
        {
            ThreadStart threadStart = PrintAsync;
            Thread thread = new Thread(threadStart);

            //추가 스레드 시작
            thread.Start();

            //현재 작업중인 스레드에서도 반복문 시작
            for (int count = 0; count < max; count++)
            {
                Console.Write("-");
            }
            Console.WriteLine("메인 스레드 끝");

            //혹시 현재 스레드가 빨리 끝나더라도,
            //추가 스레드가 끝날 때 까지 기다리기.           
            thread.Join();
        }
    }
}

<코드1> Thread와 다이다이로.

<코드1>을 보면, 맨 처음에 Task를 소개해드리면서 사용했던 예제를 Thread를 사용하도록 바꾼 코드입니다. 차이점이 있다면, ThreadStart타입의 델리게이트를 사용해야 한다는 것과, Wait()메서드가 아니라 Join()메서드를 사용한다는 것이죠. 결과를 보시면, Task를 사용했던 것과 동일합니다.

---------|||||||-|||||||||||--------------|||||||||-------------|||||------|||||||||||---------
-||||||||--------|||||||||||||-----메인 스레드 끝||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|||||||||||||||||||||||||||||||||||||||||||||||||||추가 스레드 끝
계속하려면 아무 키나 누르십시오 . . .
<결과1> Thread를 사용한 결과.

그리고 Thread를 보면, Task와 마찬가지로 실행을 제어할 수 있도록 몇가지 속성을 제공하는데요, 그 목록을 보면 아래와 같습니다.

 속성  설명 
 Join()  추가 스레드가 완료되기 전에 메인 스레드가 완료되면, 추가 스레드가 하던 작업은 다 날아간다. 그래서 추가 스레드의 작업이 완료될 때까지 메인 스레드가 기다리도록 한다.
 IsBackground  이 속성은 기본적으로 false이다. 즉, 스레드는 기본적으로 foreground작업인데, 그 때문에 스레드가 완료되기 전까지는 프로세스를 종료시킬 수 없다. 이 속성을 true로 주면, 스레드의 작업이 완료되기 전에도 프로세스를 종료시킬 수 있다.
 Priority  Join메서드를 사용한 경우에, 이 속성을 통해서 스레드의 우선순위를 바꿀 수 있다.
 ThreadState  이 속성을 통해서 스레드의 상태를 파악할 수 있는데, Aborted, AbortRequested, Background, Runnging, Stopped, StopRequested, Suspended, SuspendRequested, Unstarted, WaitSleepJoin등의 상태 값을 얻을 수 있다.
 Thread.Sleep()  현재 실행 중인 스레드의 실행을 명시한 시간만큼 일시정시 시키는 메서드이다.
 Abort()  이름 그대로, 스레드를 중지시키는 메서드. ThreadAbortException이 발생된다.
<표1> Thread의 속성.

위의 Thread멤버 중에서, Task에도 있는 건, Join()과 ThreadState뿐입니다. 왜 그럴까요? 일반적으로 권장되지 않는 것들이기 때문이죠. 그래서 닷넷 프레임워크 4.0으로 프로그래밍 할 때는, 위에서 언급한 것들 중에서 Task에 없는 속성들을 될 수 있으면 사용하지 말아야 합니다.


- ThreadPool을 사용해보자.

ThreadPool을 사용하면, 새로운 스레드를 계속 해서 생성하기 보다 기존에 있는 스레드를 재활용해서 추가적인 스레드 생성을 막을 수 있습니다. 참고로, TPL이 내부적으로 ThreadPool을 사용한다고 말씀드렸었죠? 그럼 ThreadPool을 사용하는 예제도 한번 보시죠.

using System;
using System.Threading;

namespace Exam19
{
    class Program
    {
        static readonly int max = 10000;

        public static void PrintAsync(object state)
        {
            for (int count = 0; count < max; count++)
            {
                Console.Write(state.ToString());
            }
            Console.WriteLine("추가 스레드 끝");
        }

        static void Main(string[] args)
        {
            ThreadPool.QueueUserWorkItem(PrintAsync, "|");
           
            //현재 작업중인 스레드에서도 반복문 시작
            for (int count = 0; count < max; count++)
            {
                Console.Write("-");
            }
            Console.WriteLine("메인 스레드 끝");

            //혹시 현재 스레드가 빨리 끝나더라도,
            //추가 스레드가 끝날 때 까지 기다리기.           
            Thread.Sleep(1000);
        }
    }
}

<코드2> ThreadPool을 사용한 코드.

<코드2>를 보시면, ThreadPool을 사용하고 있는데요. QueueUserWorkItem메서드를 통해서 작업을 추가하고 있습니다. 그러면, 자동으로 스레드를 활용해서 작업을 시작하게 되구요. 결과는 앞선 예제와 동일합니다. 그런데, ThreadPool을 사용할 때 장점만이 있는 건 아닌데요. 작성한 코드외에도 다른 라이브러리등에서 내부적으로 시간이 많이 걸리는 I/O작업 등에 ThreadPool을 사용한다면, 그 작업이 끝날 때까지 기다려야 하거나, 심한 경우에는 데드락이 발생하기도 합니다. 그리고 Thread나 Task를 사용할 때와는 다르게 ThreadPool은 실행 중인 작업에 접근할 수 있는 방법이 없습니다. 그래서 실행 중인 작업을 조종한다거나, 상태를 확인할 수 가 없죠. 그래서 <코드2>를 보시면, Join()이나 Wait()대신에, Thread.Sleep()메서드를 통해서 추가 스레드가 끝날 때까지 메인 스레드를 기다리게 합니다.


- 마치면서

오늘은 닷넷 3.5 까지의 멀티 스레드 프로그래밍 방법에 대해서 알아봤는데요. 크게 다른 모습은 없습니다. 다만, 좀 더 안전하고 간단한 방법을 제공하는 것이죠. 대한민국도 16강에 진출했는데 오늘은 여기까지 하시죠!...응??


- 참고자료

1. Essential C# 4.0, Mark Michaelis, Addison Wesley
저작자 표시 비영리 변경 금지
신고
- 이건 또 무슨 신개념 속담 드립인거.

저는 늘 의문을 품어왔습니다...는 훼이크고 이번 포스트를 준비하면서 의문을 가지게 되었습니다. 분명 병렬 프로그래밍의 정신은 남아도는 코어를 활용해서 협력을 해서 작업을 좀 더 빨리 끝내자고 하는 건데요, 그런면에서 '백지장도 맞들면 낫다'는 말은 병렬 프로그래밍의 정신을 잘 표현하는 선조들의 지혜라고 볼 수 있습니다. 그런데요.... 과연 백지장같이 갓난 아기도 혼자들 수 있는 걸 같이 드는게 과연 나은 일일까요? 오히려 혼자 할 때보다 못한 결과를 가져오지는 않을까요? 오늘은 그에 대한 이야기입니다.


- LINQ도 맞들면 낫다, 어헣.

LINQ는 데이터 쿼리에 가까운 표현을 사용하면서, 데이터 쿼리시에 직관적이고 선언적인 코드를 활용할 수 있도록 해주었는데요. 거기에 이전 포스트들에서 설명드렸던 Parallel.For나 Parallel.ForEach처럼 매우 간단하게 남아도는 코어를 활용할 수 있도록 하는 방법을 제공합니다.

using System;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;

namespace Exam15
{
    class Program
    {
        static void Main(string[] args)
        {
            int[] nums = Enumerable.Range(1, 10000).ToArray();

            Func<int, int> square = (num) => {
                Console.WriteLine(Task.CurrentId);
                Thread.Sleep(10);
                return num * num;
            };

            nums = nums.AsParallel()
                .Select(square)
                .ToArray();
        }
    }
}

<코드1> LINQ를 맞드는 예제.

<코드1>을 보시면, 1부터 1000까지의 숫자를 가진 배열을 생성하고, 각 수를 제곱한 수를 구하는 코드입니다. 기존의 LINQ코드와 다른 점이 있다면, 제곱 연산을 수행하기 위한 데이터 소스인 nums에 대해서 AsParallel()을 호출했다는 것입니다. <코드1>에선 AsParallel()의 리턴타입이 ParallelQuery<int>인데요, LINQ에서는 Enumerable을 사용하지만, PLINQ에서는 ParallelEnumerable을 사용합니다.

<코드1>을 보면, 정말 간단하게 병렬 프로그래밍이 구현되는데요. 정말 저렇게 간단한 방법으로 병렬 쿼리가 실행되는지 확인하기 위해서 Task.CurrentId를 통해서 실행중인 스레드의 Id를 출력하도록 했습니다. 그리고 비교적 일관성 있는 결과를 얻기 위해서 Thread.Sleep를 통해서 실행을 조금 여유롭게 해줬죠. 결과를 보실까요?

(생략)
3
1
4
2
3
1
4
2
3
1
4
2
3
1
4
2
3
1
4
2
3
1
4
2
3
1
4
2
3
1
4
2
계속하려면 아무 키나 누르십시오 . . .
<결과1> LINQ를 맞든 결과.

3->1->4->2의 패턴이 반복되는 걸 확인하실 수 있습니다. 물론, 실행도중에 패턴은 바뀌기도 합니다만, 분명 AsParallel()메서드를 호출하는 것 만으로도 병렬 프로그래밍이 구현된 것이죠. 그런데, 출력되는 스레드의 아이디를 보면, 딱 4개만 생성된 걸 확인할 수 있는데요. 제 컴퓨터의 CPU가 쿼드코어라서 딱 4개만 생성된 것 같습니다. 그런데 왜 딱 4개만 생성된 걸까요? 이전에 TPL을 활용해서 작업할 때는 4개 이상의 스레드도 생성되어서 작업을 처리했는데 말이죠. 그건 PLINQ가 병렬 쿼리를 처리하는 방식에서 원인을 찾을 수 있습니다.

제가 술을 먹고 만취한 상태에서 글을 적어서 그럴까요? 아래 내용은 새빨간 거짓말 입니다!!! 낄낄낄-_-;;; 스레드가 4개만 생성된 건, PLINQ가 분할 알고리즘으로 구간 분할을 사용하기 때문에 그렇습니다. 그리고 정확한 설명은, PLINQ는 ParallelEnumerable타입 같이 병렬 쿼리를 돌려도 안전한 타입에 대해서는 주저없이 쿼리를 병렬화 해서 작업을 하지만, IEnumerable타입 같이 병렬로 쿼리를 돌릴 때, 안전하다고 보장할 수 없는 경우에는 순차적인 쿼리로(정확히 말하지만, 순차적인 쿼리가 아니라 Chunk 분할 알고리즘을 통해서 데이터 소스에 락을 걸고, 스레드가 한번에 작업할 덩어리를 떼어주는 형태로)작업을 하게 됩니다. 오해 없으시길 바랍니다! 어헣-_-;;;

PLINQ는 AsParallel()메서드로 데이터 소스에 대해서 병렬처리를 원했다고 하더라도 항상 병렬적으로 처리를 하지는 않습니다. 예를 들면, 작업이 너무나 간단해서, 병렬적으로 처리할 때 오히려 손해를 보는경우가 있습니다. 작업이 너무 간단하기 때문에 각 스레드가 처리하는 작업의 시간이 매우 짧고, 그래서 작업 처리에 걸리는 시간보다, 스레드 간의 작업전환에 더 많은 시간이 걸리는 것이죠. 그래서 PLINQ는 AsParallel()이 호출되면, 우선 쿼리를 분석합니다. 그리고 그 쿼리가 간단하다는 판단을 하면, 병렬적으로 처리하기 보다는 순차적으로 처리를 하게 되는 것이죠. <결과1>에서 스레드가 4개가 돌아간 것은, CPU의 코어가 4개 이기 때문에, 코어별로 스레드가 한 개씩 생성된 것입니다. 각 코어의 입장에서 보자면, 스레드가 한 개씩 있는 셈이므로 작업전환이 필요없겠죠. 참고로, 듀얼 코어인 제 노트북에서 실행한 결과는 아래와 같습니다.

(생략)
1
2
1
2
1
1
2
1
2
1
2
2
1
2
1
2
1
2
1
2
1
계속하려면 아무 키나 누르십시오 . . .
<결과2> 듀얼 코어에서 맞든 LIINQ 결과.

그런가 하면, 몇 개의 스레드를 생성할 것인지 명시해 줄 수도 있는데요. <코드2>와 같이 3개의 스레드를 명시해주고 결과를 보겠습니다.

using System;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;

namespace Exam16
{
    class Program
    {
        static void Main(string[] args)
        {
            int[] nums = Enumerable.Range(1, 10000).ToArray();

            Func<int, int> square = (num) =>
            {
                Console.WriteLine(Task.CurrentId);
                Thread.Sleep(10);
                return num * num;
            };

            nums = nums.AsParallel()
                .WithDegreeOfParallelism(3)               
                .Select(square)
                .ToArray();
        }
    }
}

<코드2> LINQ를 맞들 스레드의 개수를 지정하는 코드.

(생략)
1
3
2
3
1
2
1
3
2
1
3
2
1
3
2
1
3
2
3
1
2
3
계속하려면 아무 키나 누르십시오 . . .
<결과3> 3개의 스레드로 맞든 LINQ 결과.

패턴은 약간 불안정할 때도 있지만, 대략 1->2->3의 순서를 유지하고 있습니다. 그런데, 왜 이렇게 스레드의 개수를 정해 줄 수도 있게 했을까요? 바로 최적화 때문입니다. 기본적으로 PLINQ의 알고리즘은 많은 경우를 테스트해서 최적화 알고리즘을 만들어 놓았기 때문에, 대부분의 경우는 기본옵션으로 실행하는 것이 가장 좋은 결과를 냅니다. 하지만, 그렇지 못한 경우가 있을 수 있는데요. 그럴 때, 테스트를 통해서 적절한 스레드 개수를 지정할 수 있도록 옵션을 둔 것이죠.

위에서 쿼리 식이 단순하면, 순차적으로 실행한다고 말씀을 드렸는데요, 쿼리 식이 병렬로 실행하기에 안전하지 못한 경우에, 순차적으로 실행하다고 말씀을 드렸는데요, 그런 경우도 병렬적으로 실행을 강제할 수 있습니다. 쿼리 식에 '.WithExecutionMode(ParallelExecutionMode.ForceParallelism)'메서드를 추가하면, 기본 알고리즘과는 상관없이 무조건 병렬적으로 실행하도록 합니다. 실행시간을 테스트한다거나 할때 유용하게 사용할 수 있는 옵션이겠죠.


- LINQ 맞들기 취소는 어떠케?

이번에는 PLINQ 쿼리를 취소하는 방법에 대해서 알아보겠습니다. 지금까지 취소에는 CancellationTokenSource를 활용했었죠? 마찬가지 입니다. 똑같이 Token을 활용해서 취소에 사용하되, 사용하는 방법이 조금씩 다른 것 뿐이지요.

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Exam17
{
    class Program
    {
        public static int[] SimpleParallelTask(int[] source,
            CancellationToken token)
        {
            Func<int, int> square = (num) =>
            {
                Console.WriteLine(Task.CurrentId);
                Thread.Sleep(10);
                return num * num;
            };
           
            return source.AsParallel()
                .WithCancellation(token)
                .WithDegreeOfParallelism(3)
                .Select(square)
                .ToArray();
        }

        static void Main(string[] args)
        {
            CancellationTokenSource cts =
                new CancellationTokenSource();

            Console.WriteLine("끝내려면 아무키나 누르세요");

            int[] nums = Enumerable.Range(1, 10000).ToArray();

            Task task = Task.Factory.StartNew(() =>
                {
                    nums = SimpleParallelTask(nums, cts.Token);
                }, cts.Token);

            Console.Read();

            cts.Cancel();
            Console.WriteLine("-------------------------------------");

            try
            {
                task.Wait();
            }
            catch (AggregateException)
            {
                Console.WriteLine("쿼리가 중간에 취소되었습니다.");
            }
        }
    }
}

<코드3> LINQ 맞들기 취소하기.

<코드3>을 보면, AsParallel메서드의 결과로 리턴되는 ParallelQuery타입에 포함된 .WithCancellation메서드를 사용해서 PLINQ 쿼리에 CancellationToken을 넘겨준다는 것을 제외하고는 Parallel.For, Parallel.ForEach와 동일한 방법을 사용하고 있습니다. 결과도 예측할 수 있듯이 동일합니다.

(생략)
1
3
2
1
3
2
1
3
2
1
3
2
1
3
2
1
3

-------------------------------------
쿼리가 중간에 취소되었습니다.
계속하려면 아무 키나 누르십시오 . . .

<결과4> LINQ 맞들기를 취소한 결과.


- 마치면서

어떠셨나요? '백지장도 맞들면 낫다'는 속담이 PLINQ에서는 항상 참이 아니라는 게 말이죠. 이래서 병렬 프로그래밍이 어려운가 봅니다. 어허허허헣. 악플 사절(죽을때 까지 미워할거임)! 피드백 환영! 호호호호^^


- 참고자료

1. Essential C# 4.0, Mark Michaelis, Addison Wesley
저작자 표시 비영리 변경 금지
신고

- 취소는 어렵지.

약속은 하는 것 보다, 취소하는 게 어렵습니다. 게다가 취소할 때는 적절한 타이밍을 놓치면, 안 좋은 기억만 남기게 되죠. 그래서 프로그램에서도 취소를 제대로 할 수 있도록 지원하는 게 중요합니다. 누구나 실수 할 수 있거든요.


- TPL과 함께 취소 좀 더 쉽게하기. 어헣.

TPL은 두가지 형태의 병렬성을 지원합니다. 첫 번째는 작업 병렬성(Task Parallel)이고, 두 번째는 데이터 병렬성(Data Parallelism)입니다. 작업 병렬성은 하나 이상의 작업이 동시에 진행되는 것을 말하구요, 데이터 병렬성은 연속적인 데이터에 대해서 동일한 작업이 동시적으로 수행되는 것을 말합니다. 기존까지 Task클래스와 관련해서 살펴봤던게 작업 병렬성을 위한 것이었다면, 이번에는 데이터 병렬성을 지원하는 부분을 살펴보겠습니다.

데이터 병렬성을 매우 손쉽게 지원하기 위해서 System.Threading.Tasks.Parallel클래스에 병렬성을 지원하는 for와 foreach를 추가했습니다. Parallel.For와 Parallel.ForEach가 바로 그 것인데요. 하나씩 살펴보겠습니다.

using System;
using System.Linq;
using System.Threading.Tasks;

namespace Exam13
{
    class Program
    {
        static void Main(string[] args)
        {
            int[] nums = Enumerable.Range(1, 1000).ToArray<int>();
            Parallel.For(0, 1000, (i) =>
            {
                nums[i] = nums[i] * nums[i];
            });

            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine(nums[i].ToString());               
            }
        }
    }
}

<코드1> 간단한 병렬 예제.

<코드1>을 보면, 1부터 1000까지의 정수 배열을 만든 뒤에, 각각의 수를 제곱하는 코드입니다. i번째의 숫자를 제곱해서 그 결과를 i번째 인덱스에 넣는 작업과, i+1번째의 숫자를 제곱해서 그 결과를 i+1번째 인덱스에 넣는 작업은 별개의 작업이며, 동시에 수행가능한 작업이죠. 저렇게 for와 거의 비슷한 모양으로 작성하고, for대신에 Parallel.For를 써주는 것 만으로도 남아도는 CPU의 코어를 활용할 수 있다니. 간편하죠?

Parallel.ForEach와 병렬 루프에서 예외를 처리하는 부분은 이미 다룬 부분이기 때문에 건너뛰구영. 바로, 병렬 루프를 취소하는 방법에 대해서 알아보겠습니다. 지난 포스트에서 작업을 취소하는 방법에 대해서 알아봤었는데요. 이번에도 크게 다르지 않습니다. 동일하게 CancellationTokenSource와 CancellationToken클래스를 활용합니다. 다만, 방법이 약간 다른데요, 예제를 보시죠.

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Threading.Tasks;
using System.Threading;

namespace Exam14
{
    class Program
    {
        static void Main(string[] args)
        {
            CancellationTokenSource cts =
                new CancellationTokenSource();
            ParallelOptions parallelOptions =
                new ParallelOptions
                {
                    CancellationToken = cts.Token
                };
            cts.Token.Register(
                () => Console.WriteLine("Cancelling....")
            );

            Console.WriteLine("끝내려면 엔터키를 누르세용.");

            IEnumerable<string> files =
                Directory.GetFiles("C:\\음악", "*", SearchOption.AllDirectories);
            List<string> fileList = new List<string>();

            Console.WriteLine("파일 개수 : {0}", files.Count());

            Task task = Task.Factory.StartNew(() =>
                {
                    try
                 {
                        Parallel.ForEach(files, parallelOptions,
                        (file) =>
                        {
                            FileInfo fileInfo = new FileInfo(file);
                            if (fileInfo.Exists)
                            {
                                if (fileInfo.Length >= 10000000)
                                {
                                    fileList.Add(fileInfo.Name);
                                }
                            }
                        });
                 }
                 catch (OperationCanceledException)
                 {
                 }
                });

            Console.Read();

            cts.Cancel();
            task.Wait();
           
            foreach (var file in fileList)
            {
                Console.WriteLine(file);
            }
            Console.WriteLine("총 파일 개수 : {0}",fileList.Count());
        }
    }
}

<코드2> 병렬 루프에서의 작업 취소.

<코드2>를 보면, Parallel.ForEach이용해서, 음악 파일 중에서 10메가가 넘는 파일만 찾아서 리스트에 담고 있습니다. 그리고 루프 취소와 모니터링을 위해서 CancellationTokenSource, CancellationToken클래스를 활용하고 있습니다. 다른점이 있다면, 병렬 루프에 옵션을 주기 위해서 ParallelOptions클래스를 사용하고 있다는 것이죠. 그리고 생성된 ParallelOptions타입의 객체에 Token을 주고, 그 객체를 Parallel.ForEach루프에 매개변수로 넘겨주고 있습니다. 결과를 보면, 늦게 취소를 한 경우에는 리스트가 모두 완성된 반면에, 빨리 취소를 한 경우에는 리스트가 만들어지다가 만 걸 확인할 수 있죠.

끝내려면 엔터키를 누르세용.
파일 개수 : 2746

Cancelling....

(중략...)

05 サンクチュアリ.mp3
06 空のように 海のように.mp3
07 月の虹.mp3
총 파일 개수 : 380
계속하려면 아무 키나 누르십시오 . . .

<결과1> 늦게 취소해서 다 완성된 리스트.

끝내려면 엔터키를 누르세용.
파일 개수 : 2746

Cancelling....

(중략...)

01.mp3
02.mp3
03.mp3
01.うるわしきひと.mp3
총 파일 개수 : 256
계속하려면 아무 키나 누르십시오 . . .

<결과2> 중간에 취소해서 만들어지다 만 리스트.

ParallelOptions를 통해서 CancellationToken을 받은 병렬 루프는 내부적으로, IsCancellationRequested속성을 계속해서 주시하고 있습니다. 그리고, 이 취소 요청이 들어와서 이 속성이 true가 되면, 그 이후로는 새로운 루프가 시작되는 걸 막아버리는 것이죠. 그리고 병렬 루프가 취소되었음을 외부에 알릴 수 있는 유일한 방법이 OperationCanceledException을 통해서 인데요. <코드2>를 보면, catch를 통해서 예외를 잡긴하지만, 무시해버렸습니다. 그래서 Register메서드를 통해서 등록된 "Cancelling...."이라는 메세지가 출력되고 프로그램이 종료된 것이죠.


- 마치면서

역시 병렬처리를 간단하게 만들어 주는 만큼, 병렬처리를 취소하는 방법도 최대한 간단하게 만들어 주네요. TPL만쉐이! 어헣.


- 참고자료

1. Essential C# 4.0, Mark Michaelis, Addison Wesley
2. http://msdn.microsoft.com/en-us/library/dd537608.aspx
3. http://msdn.microsoft.com/en-us/library/dd537609.aspx

저작자 표시 비영리 변경 금지
신고

- 뭐, 미안하다고?

선진국에 가보면, 약간만 부딛힐 듯한 상황이라면, 서로 'Excuse me', '스미마셍'같이 서로를 배려하는 모습을 볼 수 있습니다. 우리나라에서는 아직 길을 걸으면서 뒷 사람에게 담배연기를 선사한다던가, 뭐 그리 급한지 보행자일 때는 운전자를, 운전할 때는 보행자를 씹으면서 급하게 서두르는 모습을 쉽게 볼 수 있습니다. 층간소음이 일어나면 오히려 윗집이 더 큰소리를 치기도 하죠. 시민의식으로 겨루는 한일전에서도 완승을 거뒀으면 좋겠다는 생각을 합니다만, 저 역시 모범시민은 아니기에 같이 노력해야겠죠. 어허허허헣. 오늘은 닷넷이 예절을 얼마나 잘 지키는 지, 한번 살펴보겠습니다.


- Stop it, Now!

위 소제목을 보시고, 잭 바우어를 떠올렸다면, 24시의 팬이시겠군요. 잭 바우어는 너무나도 급한 상황을 많이 만나는데요, 상대방에게는 정말 미안하지만, 상황을 해결하기 위해서 윽박지르고, 때로는 때리고, 아주 때로는 다리를 쏘는 등등등! 의 방법을 사용합니다.

아흙. 닷넷의 멀티 스레드 환경을 한번 생각해보죠. 여러개의 스레드가 작업을 처리하는 동안, 하나의 스레드는 사용자의 UI에서 입력을 기다립니다. 그리고 사용자가 취소버튼을 누르면, 사용자의 의지를 이어받아서 다른 스레드들을 취소해야 하는데요. 기존의 .NET 3.5까지는 작업 중인 스레드를 취소하는 게 매우 무례했습니다. 취소해야 할 때는 기냥 바로 끼어들어서 취소해버렸기 때문이죠. 그렇게 하면, 데이터 업데이트가 이뤄지는 도중에 취소되어서 부분적으로만 데이터가 업데이트 된다든지, 자원해제가 제대로 안 된다든지 하는 부작용의 위험이 항상 존재합니다. 그래서 가능하면, 다른 방법이 전혀 없을 때, 이렇게 하는 것이 좋겠죠?

물론 기존의 방식도 여전히 활용가능하지만, 이젠 닷넷이 많이 예의를 갖췄습니다. 닷넷 4.0에 새롭게 추가된 PLINQ나 TPL을 사용하는 경우에는 취소 요청 접근법(cancellation request approach)만 사용할 수 있는데요, 이런 방식을 협력적인 취소(cooperative cancellation)이라고 합니다. 즉, 한 스레드가 다른 스레드를 강제로 종료시키는 게 아니라, 작업 취소 API를 통해서 작업을 취소해줄 것을 요청하는 것이죠. 취소 플래그를 통해서 취소요청을 받은 작업은 취소요청에 어떻게 응답할 것인지 선택할 수 있습니다. 예제를 하나 보시죠.

using System;
using System.Threading.Tasks;
using System.Threading;

namespace Exam11
{
    class Program
    {
        static void PrintDash(CancellationToken cancellationToken)
        {
            cancellationToken.Register(Canceled);

            while (!cancellationToken.IsCancellationRequested)
            {
                Console.Write("-");
            }
        }

        static void Canceled()
        {
            Console.WriteLine("작업이 취소되었군요!!");
        }

        static void Main(string[] args)
        {
            string stars = "*".PadRight(Console.WindowWidth - 1, '*');

            CancellationTokenSource cancellationTokenSource =
                new CancellationTokenSource();

            Task task = Task.Factory.StartNew(
                () => PrintDash(cancellationTokenSource.Token));
           
            Console.ReadLine();

            cancellationTokenSource.Cancel();
            Console.WriteLine(stars);
            task.Wait();
            Console.WriteLine("작업의 완료상태 : {0}", task.Status);
            Console.WriteLine();
        }
    }
}

<코드1> 취소 요청 접근법.

<코드1>은 그냥 평범하게 '-'를 출력하는 예제입니다. 하지만, 새로운 클래스가 몇개 보이는데요. CancellationTokenSource, CancellationToken클래스말이죠.

namespace System.Threading
{
    [ComVisible(false)]
    [DebuggerDisplay("IsCancellationRequested = {IsCancellationRequested}")]
    public struct CancellationToken
    {
        public CancellationToken(bool canceled);
       
        public static bool operator !=(CancellationToken left, CancellationToken right);

        public static bool operator ==(CancellationToken left, CancellationToken right);

        public bool CanBeCanceled { get; }

        public bool IsCancellationRequested { get; }

        public static CancellationToken None { get; }

        public WaitHandle WaitHandle { get; }

        public bool Equals(CancellationToken other);

        public override bool Equals(object other);

        public override int GetHashCode();

        public CancellationTokenRegistration Register(Action callback);

        public CancellationTokenRegistration Register(Action<object> callback, object state);

        public CancellationTokenRegistration Register(Action callback, bool useSynchronizationContext);

        public CancellationTokenRegistration Register(Action<object> callback, object state, bool useSynchronizationContext);

        public void ThrowIfCancellationRequested();
    }
}

<코드2> 구조체 CancellationToken.

CancellationToken클래스는 말 그대로, 현재 이 토큰이 어떤 상태에 있는지 모니터링 하기 위한 정보를 갖고 있습니다. 이 토큰이 현재 취소 요청을 받았는지, 취소요청을 받으면 어떤 행동을 취할 것인지 등을 확인하고, 설정할 수 있습니다.

namespace System.Threading
{
    [ComVisible(false)]
    public sealed class CancellationTokenSource : IDisposable
    {
        public CancellationTokenSource();

        public bool IsCancellationRequested { get; }

        public CancellationToken Token { get; }

        public void Cancel();

        public void Cancel(bool throwOnFirstException);

        public static CancellationTokenSource CreateLinkedTokenSource(params CancellationToken[] tokens);

        public static CancellationTokenSource CreateLinkedTokenSource(CancellationToken token1, CancellationToken token2);

        public void Dispose();
    }
}

<코드3> CancellationTokenSource 클래스

그리고 CancellationTokenSource는 CancellationToken의 기반이 되는 클래스로(Source라는 이름이 붙어있죠), CancellationTokenSource에서 생성된 각각의 Token에 대해서 취소를 요청하는 역할을 합니다. CancellationTokenSource에서 Cancel메서드로 취소요청을 하면, 같은CancellationTokenSource에서 생성된 Token들은 전부 취소요청을 받는 셈이죠.


한가지 주목해서 보실 점은 CancellationToken가 클래스가 아니라 구조체라는 것입니다. 즉, Token을 매번 다른 객체에 넘겨줄 때마다 새로운 복사본이 생성된다는 것이죠. 그래서 각각의 스레드에 넘겨진 Token은 각각 독립적인 복사본 이므로, Cancel메서드는 스레드 안전성(thread-safe)을 확보할 수 있습니다. 만약에 참조가 그냥 복사된다면, 각각의 스레드가 Token에 손을 대면, 다른 스레드가 참조하는 Token에도 동일한 변화가 생겨서 예측불가능한 일이 벌어지겠죠.

<코드1>을 보면, 병렬적으로 수행되는 작업에서 취소 요청을 모니터링하기 위해서, CancellationToken을 인자로 넘겨주는 것을 볼 수 있습니다. 그래서 PrintDash메서드 내부에서 IsCancellationRequested속성을 통해서 작업 취소 요청이 들어왔는지 계속 해서 확인하게 되죠. 그럼 <코드1>을 실행 해볼까요?

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--작업이 취소되었군요!!
*******************************************************************************
작업의 완료상태 : RanToCompletion

계속하려면 아무 키나 누르십시오 . . .

<결과1> <코드1>의 실행결과.

<결과1>을 보면, 작업의 완료상태를 출력하는 부분이 있는데요, 이 부분에서 RanToCompletion이 출력되고 있습니다. 그래서 만약, ContinueWith메서드로 연쇄 작업을 연결하고, 옵션을 OnlyOnCanceled로 설정해준다고 하더라도, 연쇄작업은 실행되지 않습니다. 작업은 완료된 상태이기 때문에, 연쇄 작업이 취소되었다는 에러메세지만 확인할 수 있을 뿐이죠. 그렇다면, 연쇄작업을 이용해서 <코드1>과 동일한 결과를 내려면 어떻게 해야 할까요?

using System;
using System.Threading.Tasks;
using System.Threading;

namespace Exam12
{
    class Program
    {
        static void PrintDash(CancellationToken cancellationToken)
        {
            while (!cancellationToken.IsCancellationRequested)
            {
                Console.Write("-");
            }

            if (cancellationToken.IsCancellationRequested)
            {
                cancellationToken.ThrowIfCancellationRequested();
            }
        }

        static void Main(string[] args)
        {
            string stars = "*".PadRight(Console.WindowWidth - 1, '*');

            CancellationTokenSource cancellationTokenSource =
                new CancellationTokenSource();

            Task task = Task.Factory.StartNew(
                () => PrintDash(cancellationTokenSource.Token),
                cancellationTokenSource.Token);

            Task canceledTask = task.ContinueWith(
                (antecedentTask) => Console.WriteLine("작업이 취소되었군요!!"),
                TaskContinuationOptions.OnlyOnCanceled);

            Console.ReadLine();

            cancellationTokenSource.Cancel();
            Console.WriteLine(stars);
            canceledTask.Wait();
            Console.WriteLine("작업의 완료상태 : {0}", task.Status);
            Console.WriteLine();
        }
    }
}

<코드4> <코드1>과 동일하지만, 연쇄작업을 사용하는 코드.

<코드4>가 바로, <코드1>과 동일한 결과를 내는 코드입니다.(사실 완전히 같지는 않습니다. 실행해보면, '작업이 취소되었군요!'라는 멘트가 출력되는 위치가 다르지요.) 이런식으로 연쇄작업을 연결해놓고, 병렬로 실행되는 메서드 안에서, ThrowIfCancellationRequested()메서드를 통해서, 취소되었을 때, 취소되었다는 표시를 하도록 한 것이죠. 그러면, 연쇄 작업이 바톤을 이어받아서 실행을 계속하게 됩니다. 그리고 또 한가지 차이점은 작업을 생성할 때, 인자로 Token을 넘겨준다는 것이지요.


- 마치면서.

요즘 월드컵을 보면 16강이 가능할 것 같다는 생각이 들기도 하는데요. 꼭! 갔으면 좋겠네요!! ......이게 마무리 멘트라니-_-!! 어헣.


- 참고자료

1. Essential C# 4.0, Mark Michaelis, Addison Wesley

저작자 표시 비영리 변경 금지
신고

- To be continue??

한 때, 뮤직비디오를 드라마 처럼 만드는 게 유행했던 때가 있었습니다. 기억하시나요? 한창 조성모가 데뷔했을 때로 기억하는데요. 노래 한곡에 뮤직비디오가 여러편이 있어서, 하나가 공개 되고 나면, 'To be continue..'라는 자막이 나오면서 애간장을 태우곤 했었죠. 그게 오늘 내용과 무슨 상관이냐!?!? 별 상관없습니다-_- 어허허헣.


- 뒤를 부탁해, ContinueWith

지금까지는 작업을 만들어서 돌리고, 작업이 종료되면 그냥 끝났었는데요. 한 작업이 어떤 상태로 끝났을 때, 그 상태에 따라서 다른 작업이 연쇄적으로 일어나도록 설정하고 싶은 경우가 있겠죠. Task클래스에는 그런 경우를 위해서 ContinueWith라는 메서드가 준비되어 있습니다. 낄낄낄. 이 메서드는 선행 작업(antecedent task)에 하나 이상의 연쇄적인 작업을 연결 시켜놓고, 선행 작업의 상태나 종료에 따라서 연쇄적으로 이후 작업이 실행되도록 설정할 수 있도록 해줍니다. 그러면, 선행 작업의 상태에 대해서 설정가능한 상태가 뭐가 있는지 한번 확인해볼까요?

 상태  설명 
 None  아무 값도 명시되지 않으면 사용되는 기본 값으로, 선행작업의 상태와 관계없이, 선행작업이 종료되면 바로 시작된다.
 NotOnRanToCompletion  선행작업이 끝난 상태에서는 연쇄작업을 실행계획에 넣지 않는다.
 NotOnFaulted  선행작업이 처리안된 예외를 던지면, 연쇄작업을 실행계획에 넣지 않는다.
 NotOnCanceled  선행작업이 취소되면, 연쇄작업을 실행계획에 넣지 않는다.
 OnlyOnRanToCompletion  선행작업이 정상적으로 완료된 상태에서만 연쇄작업을 실행계획에 추가시킨다.
 OnlyOnFaulted  선행작업이 처리안된 예외를 던지는 상태에서만 연쇄작업을 실행계획에 추가시킨다.
 OnlyOnCanceled  선행작업이 취소된 상태에서만 연쇄작업을 실행계획에 추가시킨다.
<표1> http://msdn.microsoft.com/en-us/library/system.threading.tasks.taskcontinuationoptions.aspx에서 참조한 상태값과 설명

그리고 ContinueWith메서드는 Task타입의 객체를 리턴하는 데요, 그 덕분에 연쇄적으로 작업에 작업을 계속해서 추가할 수도 있습니다. 그러면, 예제를 한번 확인해볼까요?

using System;
using System.Threading.Tasks;

namespace Exam9
{
    class Program
    {
        static string Calc(object from)
        {
            long sum = 0;
            long start = (long)from;
            Console.WriteLine("현재 이 메서드를 실행중인 스레드 ID : {0}",
                Task.CurrentId);

            for (long i = start; i < 100000000; i++)
            {
                sum += i;
                if(i == 100000 || i == 200000)
                {
                    //100000에 이르면, 그냥 실행을 종료시킨다.
                    throw new ApplicationException("그냥 에러가 났음");
                }
            }
            Console.WriteLine("계산 끝");
            return sum.ToString();
        }

        static void Main(string[] args)
        {
            Task<string> task = new Task<string>(
                Calc, 1L);

            task.Start();

            Task<string> justDoIt = task.ContinueWith<string>(
                (antecedentTask) =>
                {
                    Console.WriteLine("이전 작업 상태 : {0}", antecedentTask.Status);
                    return Calc(100001L);
                },
                TaskContinuationOptions.None);

            Task<string> justDoIt2 = justDoIt.ContinueWith<string>(
                (antecedentTask) =>
                {
                    Console.WriteLine("이전 작업 상태 : {0}", antecedentTask.Status);
                    return Calc(200001L);
                },
                TaskContinuationOptions.None);

            try
            {
                Console.WriteLine(task.Result);
            }
            catch (AggregateException ex)
            {
                foreach (var item in ex.InnerExceptions)
                {
                    Console.WriteLine("에러 : {0}", item.Message);
                }
            }
            finally
            {
                try
                {
                    Console.WriteLine(justDoIt.Result);
                }
                catch (AggregateException ex)
                {
                    foreach (var item in ex.InnerExceptions)
                    {
                        Console.WriteLine("에러 : {0}", item.Message);
                    }
                }
                finally
                {
                    Console.WriteLine(justDoIt2.Result);
                    Console.WriteLine("끝");
                }
            }
        }
    }
}

<코드1> 연쇄작업 물리기.

<코드1>은 Calc메서드를 실행하는 작업을 하나 시작하고, 그 작업에 연쇄작업을 하나 연결하고, 그 연쇄작업에 또 연쇄작업을 하나 연결한 예제입니다. 연쇄작업의 옵션을 보면, 전부 'TaskContinuationOptions.None'로 되어 있는데요. <표1>에서 확인해보면, 선행작업이 어떤 식으로 종료되든, 종료가 되면 이어서 실행하도록 하는 옵션이죠. 그리고 Calc메서드를 보면, for루프의 카운트가 100000과 200000에 이르면, 예외를 던지고 실행을 종료하도록 했습니다. 그리고 작업을 보면, task는 1부터, justDoIt은 100001부터 실행하므로 둘다 예외를 만나겠죠. 그리고 마지막 justDoIt2는 200001부터 실행하므로 실행을 종요하게 됩니다. 그럼 결과를 보죠.(설명을 위한 예제이므로 실제로 이렇게 짜면 곤란하겠죠)

현재 이 메서드를 실행중인 스레드 ID : 1
이전 작업 상태 : Faulted
현재 이 메서드를 실행중인 스레드 ID : 2
이전 작업 상태 : Faulted
현재 이 메서드를 실행중인 스레드 ID : 3
에러 : 그냥 에러가 났음
에러 : 그냥 에러가 났음
계산 끝
4999979949900000

계속하려면 아무 키나 누르십시오 . . .
<결과1> 연쇄작업 실행 결과.

우리가 예측한대로, 선행작업이 실패하면서, 바로 다음작업이 연쇄적으로 실행되는 걸 확인할 수 있습니다. 지난 포스트에서 Wait메서드를 통해서 추가적으로 생성된 스레드의 결과를 기다리거나, Result속성을 통해서 결과값을 요청하지 않으면 추가스레드의 결과는 그냥 무시된다고 했었는데요. 연쇄작업도 마찬가지 입니다. 연쇄작업을 연결해 놓았다고 해도, Wait로 기다리거나 Result를 요구하지 않으면 그냥 무시되어 버리는 것이죠. <코드1>에서 justDoIt2.Result부분을 아래 처럼 주석처리하고 실행해보죠.

finally
{
    //Console.WriteLine(justDoIt2.Result);
    Console.WriteLine("끝");
}
<코드2> 주석 처리.

그리고 결과는 보면,

현재 이 메서드를 실행중인 스레드 ID : 1
이전 작업 상태 : Faulted
현재 이 메서드를 실행중인 스레드 ID : 2
이전 작업 상태 : Faulted
현재 이 메서드를 실행중인 스레드 ID : 3
에러 : 그냥 에러가 났음
에러 : 그냥 에러가 났음

계속하려면 아무 키나 누르십시오 . . .
<결과2> 작업이 무시된 결과

justDoIt2가 진행하던 작업의 결과는 무시된 걸 확인할 수 있습니다.

<표1>에서 None옵션은 선행작업이 어떤 이유로 종료가 되었든, 종료가 되었다면 바로 이어서 연쇄작업을 실행하도록 하는 옵션인데요. 그러면, <코드1>과 동일한 결과를 가져오려면 어떤 옵션을 사용하면 되는 걸까요? 한번 생각해보시죠. 어헣. 힌트는 예외를 던지고 종료되는 작업의 상태를 보시면 바로 정답이 나옵니당. 정답은 <코드3>을 보시죠!

Task<string> justDoIt = task.ContinueWith<string>(
    (antecedentTask) =>
    {
        Console.WriteLine("이전 작업 상태 : {0}", antecedentTask.Status);
        return Calc(100001L);
    },
    TaskContinuationOptions.OnlyOnFaulted);

Task<string> justDoIt2 = justDoIt.ContinueWith<string>(
    (antecedentTask) =>
    {
        Console.WriteLine("이전 작업 상태 : {0}", antecedentTask.Status);
        return Calc(200001L);
    },
    TaskContinuationOptions.OnlyOnFaulted);

<코드3> 같은 결과를 가져오는 코드

그렇씁니다. OnlyOnFaulted는 선행작업이 처리안된 예외를 던지는 상태에서만 연쇄작업을 실행시키는 옵션이죠. 어허허허헣.

그렇다면, 작업의 성공이나 실패를 자동으로 통지하도록 하는 방법을 사용할 수 있을 것 같습니다.

using System;
using System.Threading.Tasks;

namespace Exam10
{
    class Program
    {
        static string Calc(object from)
        {
            long sum = 0;
            long start = (long)from;
            Console.WriteLine("현재 이 메서드를 실행중인 스레드 ID : {0}",
                Task.CurrentId);

            for (long i = start; i < 100000000; i++)
            {
                sum += i;
                if (i == 100000)
                {
                    //100000에 이르면, 그냥 실행을 종료시킨다.
                    throw new ApplicationException("그냥 에러가 났음");
                }
            }
            Console.WriteLine("계산 끝");
            return sum.ToString();
        }

        static void Main(string[] args)
        {
            Task<string> task = new Task<string>(
                Calc, 1L);

            task.Start();

            Task completedTask = task.ContinueWith(
                (antecedentTask) =>
                {
                    Console.WriteLine("Task State: 무사 완료!!");
                    //작업 완료 후에 이어서 뭔가를 처리
                },
                TaskContinuationOptions.OnlyOnRanToCompletion);

            Task faultedTask = task.ContinueWith(
                (antecedentTask) =>
                {
                    Console.WriteLine("Task State: 처리되지 않은 예외 발견!!");
                },
                TaskContinuationOptions.OnlyOnFaulted);

            try
            {
                completedTask.Wait();
                faultedTask.Wait();
            }
            catch (AggregateException ex)
            {
                foreach (var item in ex.InnerExceptions)
                {
                    Console.WriteLine("에러 : {0}", item.Message);
                }
            }
        }
    }
}

<코드4> 연쇄 작업으로 상태를 감지

<코드4>를 보시면, 작업이 무사히 끝났을 때와 처리안된 예외가 생겨서 종료될 때에 실행되도록 연쇄작업을 두개 연결해 놓았습니다. 일단 결과를 보면요,

현재 이 메서드를 실행중인 스레드 ID : 1
Task State: 처리되지 않은 예외 발견!!
에러 : 작업이 취소되었습니다.
계속하려면 아무 키나 누르십시오 . . .
<결과3>

예상대로, OnlyOnFaulted로 설정된 연쇄작업이 실행된 것을 확인할 수 있습니다. 왜냐면 작업을 1부터 시작하도록 했기 때문에, 카운트가 100000에 다다르면, 처리안된 예외가 발생하기 때문이죠. 작업의 상태값을 100001L이상으로 설정하면, OnlyOnRanToCompletion으로 설정된 연쇄작업이 실행되겠죠.

그런데, 두 경우다 예외가 하나씩 잡히는 걸 확인할 수 있습니다. '작업이 취소되었습니다'라는 예외인데요. 이유는 간단합니다. task에 OnlyOnRanToCompletion와 OnlyOnFaulted인 연쇄작업 두개를 연결했기 때문에, 둘 중 하나만 항상 실행이 됩니다. 그래서 둘 중에 하나는 실행이 되지 못한채, 선행작업이 끝나버리는 거죠. 그래서 Wait메서드를 통해서 기다리고 있던 작업은 종료되지 못하고 취소가 되는 마는 것이죠.


- 마무리

오늘은 선행작업의 상태에 따라서 연쇄적으로 작업을 실행하는 예제를 살펴봤습니다. 다음에 뵙쬬!


- 참고자료

1. Essential C# 4.0, Mark Michaelis, Addison Wesley

저작자 표시 비영리 변경 금지
신고

- 인생이 원하는 대로 가지는 않더라.

우리는 인생을 살면서, 여러가지 계획을 세웁니다. 하지만, 보통 계획을 세울 때, 예외적인 상황을 감안하지 않는 경우가 많습니다. 그래서 늘 일정은 실패로 끝나게 되고, '아, 난 안되나 보다.'하고 절망하게 되는 것이죠. 자세한 내용은 최인철 교수님의 '프레임'을 참고하시면..... 순간 책 소개 코너로 빠져들뻔 했군요. 어헣.

아무튼, 우리는 인생에서 뿐만 아니라 프로그래밍에서도 생각외의 순간을 많이 만나게 됩니다. '도대체 어떤 **가 이런 거 까지 해볼까?'하는 생각으로 안이하게 프로그래밍을 하다보면 기상 천외한 버그 리포트를 받게 됩니다. 그래서 예외 처리가 중요한 것이죠. 오늘은 병렬 프로그래밍에서 예외 처리 하는 법에 대해서 간단하게 이야기를 해보려고 합니다.


- 차이점 하나.

기존의 프로그래밍에서는 그저 예외를 발생시 처리하고 싶은 구문을 try로 감싸면 되었는데요. 병렬 프로그래밍에서는 어떨까요? Task.Start()를 try로 감싸면 결과를 얻을 수 있을까요? 한번 실험해보죠.

using System;
using System.Threading.Tasks;

namespace Exam5
{
    class Program
    {
        static string Calc(object from)
        {
            long sum = 0;
            long start = (long)from;
            Console.WriteLine("현재 이 메서드를 실행중인 스레드 ID : {0}",
                Task.CurrentId);

            for (long i = start; i < 100000000; i++)
            {
                sum += i;
                if (i == 100000)
                {
                    //100000에 이르면, 그냥 예외를 던진다ㅋ.
                    throw new ApplicationException("그냥 에러가 났음");
                }
            }
            Console.WriteLine("계산 끝");
            return sum.ToString();
        }

        static void Main(string[] args)
        {
            Task<string> task = new Task<string>(
                Calc, 1L);

            try
            {
                task.Start();
            }
            catch (AggregateException ex)
            {
                foreach (var item in ex.InnerExceptions)
                {
                    Console.WriteLine("에러 : {0}", item.Message);
                }
            }

            Console.WriteLine(task.Result);
        }
    }
}

<코드1> Start를 try로 감싸기

<코드1>을 보시면, task.Start()를 try로 감싸고 있습니다. 과연 실행중에 던지는 예외를 잘 받을 수 있을까요? 결과는 아래와 같습니다.

현재 이 메서드를 실행중인 스레드 ID : 1

처리되지 않은 예외: System.AggregateException: 하나 이상의 오류가 발생했습니다.
---> System.ApplicationException: 그냥 에러가 났음
   위치: Exam5.Program.Calc(Object from) 파일 C:\Users\boram\Documents\Visual St
udio 10\Projects\Chapter9\Exam7\Program.cs:줄 21
   위치: System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)
   위치: System.Threading.Tasks.Task.InnerInvoke()
   위치: System.Threading.Tasks.Task.Execute()
   --- 내부 예외 스택 추적의 끝 ---
   위치: System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCance
ledExceptions)
   위치: System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, Cancellatio
nToken cancellationToken)
   위치: System.Threading.Tasks.Task`1.get_Result()
   위치: Exam5.Program.Main(String[] args) 파일 C:\Users\boram\Documents\Visual
Studio 10\Projects\Chapter9\Exam7\Program.cs:줄 45
계속하려면 아무 키나 누르십시오 . . .

<결과1> <코드1>의 실행 결과

<결과1>을 보면, 예외는 전혀 잡히지 않았습니다. 왜 일까요? 작업에서 실행하는 코드(여기서는 Calc메서드)는 Start메서드 내에서 실행되는 게 아니라, Start메서드로 작업이 시작된 이후에야 별도로 시작되기 때문이죠. 그래서 Start메서드에 try를 걸어봤자 예외는 잡을 수 없습니다.

가만히 생각해보면, 작업 안에서 처리되는 예외(즉, Calc메서드 내에서 발생하고 처리되는 예외)는 전혀 고민할 필요가 없겠죠. 하지만, 처리되지 못한 예외가 발생하는 경우는 바깥에서 처리할 방법을 찾아야 합니다.

CLR 2.0 버전까지는 이런 처리되지 못한 예외가 발생하면, 예외가 버블링되면서, 상위 계층으로 전파되면서 결국에는 윈도우 에러 보고 대화상자를 열게 만들고, 프로그램은 종료되었습니다. 하지만, 작업 내부에서 처리되지 못한 예외라고 하더라도, 작업 바깥에서 처리할 수 있는 방법이 있다면, 프로그램이 종료되는 것 보다는 바깥에서 처리할 수 있게 하는 게 더 나은 방법이겠죠.

작업내부에서 처리 안된 예외(unhandled exception)가 발생했다면, 그 예외는 일단 작업을 마무리 짓는 멤버(Wait(), Result, Task.WaitAll(), Task.WaitAny())가 호출되기 전까지는 조용히 기다립니다. 그리고 마무리 멤버의 호출에서 처리 안된 예외가 발생하게 되는 것이죠. <코드1>에서 Start메서드를 try블록으로 감쌌지만, 예외를 잡을 수 없었던 이유가 바로 여기에 있습니다. 처리 안된 예외는 마무리 멤버와 함께 발생하기 때문이죠. 그러면, <코드1>을 수정해서, 예외를 제대로 잡도록 수정해보겠습니다.

task.Start();

try
{
    Console.WriteLine(task.Result);
}

catch (AggregateException ex)
{
    foreach (var item in ex.InnerExceptions)
    {
        Console.WriteLine("에러 : {0}", item.Message);
    }
}

<코드2> Result를 try로 감싸라.

<코드2>를 보면, 작업을 마무리 짓는 멤버 중의 하나인, Result를 try블록으로 감싸고 있습니다. 그리고 결과를 보면,

현재 이 메서드를 실행중인 스레드 ID : 1
에러 : 그냥 에러가 났음
계속하려면 아무 키나 누르십시오 . . .
<결과2> 제대로 처리된 예외.

예외가 제대로 처리 된 것을 확인할 수 있습니다.


- 차이점 둘.

혹시 <코드1>, <코드2>를 주의 깊게 보신 분이라면, 처음보는 예외 하나를 발견하셨을지도 모르겠습니다. 바로 AggregateException인데요, 이에 대해서 이야기를 좀 해보겠습니다.

AggregateException은 닷넷 프레임워크 4.0에서 처음 추가된 예외 타입인데요, MSDN의 설명을 보면(http://msdn.microsoft.com/en-us/library/system.aggregateexception.aspx), '프로그램 실행 중에 발생하는 하나 또는 여러개의 에러를 표현하는 수단'이며, 주로 TPL과 PLINQ에서 활용되고 있다고 합니다.

aggregate는 여러 개의 작은 것들을 서로 합치는 이미지를 갖고 있는데요, AggregateException은 그렇다면 여러 개의 예외를 하나로 합치는 예외타입이라는 말이 됩니다. 그런데, 여러 개의 예외는 어디서 오는 걸까요? 예전에 작성했던 예제를 조금 수정해서 확인 해보도록 하죠.

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Threading.Tasks;

namespace Exam7
{
    class Program
    {
        static void Main(string[] args)
        {
            IEnumerable<string> files =
                Directory.GetFiles("C:\\음악", "*", SearchOption.AllDirectories);
            List<string> fileList = new List<string>();

            Console.WriteLine("파일 개수 : {0}", files.Count());
           
            Parallel.ForEach(files, (file) =>
            {
                FileInfo fileInfo = new FileInfo(file);
                if (fileInfo.Exists)
                {
                    if (fileInfo.Length >= 15000000)
                    {
                        throw new ApplicationException("15메가 넘는 파일이!!");
                    }
                    else if (fileInfo.Length >= 1000000)
                    {
                        fileList.Add(fileInfo.Name);
                    }
                    Console.Write("{0}", Task.CurrentId.ToString());                   
                }
            });
        }
    }
}

<코드3> 약간 수정된 예제.

<코드3>이 바로 그 예제인데요, 예제를 보면, 음악 폴더에서 15메가 넘는 파일이 발견되면, 예외를 발생하도록 되어있습니다. flac같은 파일로 보자면, 15메가 넘는 파일은 흔히 있겠지만, 저는 서민이라 mp3를 선호합니다. 어헣-_-. 아무튼, 이 예제를 실행 시켜보면요, 간혹 15메가 넘는 파일이 몇개는 있기 마련이기 때문에, 실행 중에 에러가 나게 되어 있습니다. 한번 디버깅을 해보죠.


<그림1> 병렬 스택 창

<그림1>의 병렬 스택을 보시면요, 병렬 ForEach문에 의해서 4개의 작업자 스레드가 생성된 걸 확인할 수 있습니다.(리스트의 크기나, CPU자원 상태등에 따라서 개수는 계속해서 변합니다.)

<그림2> 병렬 작업 창

그리고 <그림2>의 병렬 작업창을 보면, 2번 스레드가 15메가 넘는 파일을 만난 것을 확인할 수 있습니다. 그러면, 2번 스레드가 예외를 던질텐데, 나머지 작업은 어떻게 될까요? 처리 되지 않은 예외가 발생하는 순간, 다른 작업들은 모두 날아가버립니다.

그런데, 각 스레드 별로 파일 리스트를 쪼개서 줬을 텐데요. 각 스레드가 각자의 목록을 가지고 작업을 하다보면, 각 스레드 별로 15메가가 넘는 파일을 발견하게 될 것입니다. 이런 예외들을 만날 때 마다 처리하지 말고, 모두 모아서 한번에 처리하려면 어떻게 해야 할까요? 그래서 바로 AggregateException을 사용하는 거죠.

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Threading.Tasks;
using System.Collections.Concurrent;

namespace Exam8
{
    class Program
    {
        static void Main(string[] args)
        {
            IEnumerable<string> files =
                Directory.GetFiles("C:\\음악", "*", SearchOption.AllDirectories);
            List<string> fileList = new List<string>();

            Console.WriteLine("파일 개수 : {0}", files.Count());

            var exceptions = new ConcurrentQueue<Exception>();

            try
            {
                Parallel.ForEach(files, (file) =>
                {
                    FileInfo fileInfo = new FileInfo(file);
                    if (fileInfo.Exists)
                    {
                        try
                        {
                            if (fileInfo.Length >= 15000000)
                            {
                                throw new ApplicationException("15메가 넘는 파일이!!");
                            }
                            else if (fileInfo.Length >= 1000000)
                            {
                                fileList.Add(fileInfo.Name);
                            }
                            Console.Write("{0}", Task.CurrentId.ToString());
                        }
                        catch (Exception ex)
                        {
                            exceptions.Enqueue(ex);
                        }
                    }
                });

                throw new AggregateException(exceptions);
            }
            catch (AggregateException ex)
            {
                foreach (var item in ex.InnerExceptions)
                {
                    Console.WriteLine("\n에러 : {0}", item.Message);
                }

                Console.Write("\n파일 리스트 계속해서 보기(엔터키를 치세요)");
                Console.ReadLine();
            }
            finally
            {
                foreach (string file in fileList)
                {
                    Console.WriteLine(file);
                }

                Console.WriteLine("총 파일 개수 : {0}", fileList.Count());
            }
        }
    }
}

<코드4> AggregateException을 사용.

ConcurrentQueue는 Queue긴 하지만, 여러 스레드에 의해서 동시에 큐에 추가를 하거나 해도 안전하도록 만들어진(thread-safe) Queue입니다. 그 큐에서 예외가 발생할 때마다, 예외를 저장해뒀다가, 한꺼번에 AggregateException으로 던지는 것이죠. 그리고 바깥쪽의 catch블록에서 예외를 받아서 내부의 예외 목록을 하나씩 처리하는 것입니다.

(생략)
에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

파일 리스트 계속해서 보기(엔터키를 치세요)

<결과 3> 처리과정에서 생긴 예외를 모두 모아서 처리.


- 마무리.

오늘 예외처리에 대해서 봤습니다. 다음은~? 작업을 연쇄적으로 처리할 수 있도록 하는 부분을 보겠습니돠. 그럼 평안하시길. 어허허허허헣.


- 참고자료

1. Essential C# 4.0, Mark Michaelis, Addison Wesley
2. http://msdn.microsoft.com/en-us/library/dd460695.aspx
3. http://msdn.microsoft.com/en-us/magazine/ee321571.aspx
4. http://msdn.microsoft.com/en-us/library/system.aggregateexception.aspx

저작자 표시 비영리 변경 금지
신고

- 잡설없이 바로 고고고!

지난 포스트의 마지막 예제에서 Task 클래스의 IsCompleted라는 속성을 사용했었습니다. 이름 그대로, 해당 작업이 끝났는지를 확인할 수 있는 속성인거죠. 이런 속성말고는 또 뭐가 있을까요? 몇 가지 쓸모 있는 속성을 들을 예제를 통해서 확인해보도록 하죠.

using System;
using System.Threading.Tasks;

namespace Exam4
{
    class Program
    {
        static string Calc(object from)
        {
            long sum = 0;
            long start = (long)from;
            Console.WriteLine("현재 이 메서드를 실행중인 스레드 ID : {0}",
                Task.CurrentId);

            for (long i = start; i < 100000000; i++)
            {
                sum += i;
            }
            return sum.ToString();
        }

        static void Main(string[] args)
        {
            Task<string> task = new Task<string>(Calc, 1L);

            task.Start();

            Console.WriteLine("추가 스레드의 ID {0}",
                task.Id);

            Console.WriteLine("추가 스레드에 제공된 상태 값 : {0}",
                task.AsyncState.ToString());

            while (!task.IsCompleted)
            {
                if (task.Status == TaskStatus.Running)
                {
                    Console.Write(".");
                }
            }

            Console.WriteLine("");
            //여기서 추가 스레드가 끝날 때 까지 기다린다.
            Console.WriteLine(task.Result);
            System.Diagnostics.Trace.Assert(
                task.IsCompleted);
        }
    }
}

<코드1> 유용한 속성을 사용한 예제

<코드1>은 기본적으로 유용하게 사용되는 속성들을 활용한 간단한 예제입니다. 우선 결과를 볼까요?

추가 스레드의 ID 1
현재 이 메서드를 실행중인 스레드 ID : 1
추가 스레드에 제공된 상태 값 : 1
................................................................................................................................................................
................................................................................................................................................................
................................................................................................................................................................
................................................................................................................................................................
................................................................................................................................................................
................................................................................................................................................................
...............................................................................................................................................................
................................................................................................................................................................
-중략-
................................................................................................................................................................
......................................................................................
4999999950000000
계속하려면 아무 키나 누르십시오 . . .
<결과1> 실행 결과

그럼 속성을 하나씩 확인 해보면서 왜 저런 결과가 나왔는지 확인해보죠.

 속성명  설명 
 Task.CurrentId  현재 Task.CurrentId 호출을 처리하는 스레드의 Id
 AsyncState  스레드에 추척가능한 값을 부여한다. 예를 들어서 스레드가 3개가 있고, 각각 1,2,3 이라는 값을 부여했다면, 각각의 스레드의 AsyncState는 1,2,3 이므로 이 값을 다른 용도로 사용할 수도 있고, 스레드를 구별하는데 사용할 수도 있다. 
 Id  각각의 작업에 부여되는 고유한 Id
 Status  현재 작업의 상태를 표시한다. Created, WatingForActivation, WaitingForRun, Running, WaitingForChildrenToComplete, RanToComplete, Canceled, Faulted등이 있다. 
<표1> 유용한 속성들

네 각각의 속성을 한번 정리해봤습니다. 어헣. 추가 스레드의 아이디는 1이었죠. 그리고, Calc메서드를 실행하는 스레드도 역시 아이디가 1인 추가 스레드이기 때문에, Calc메서드 안에서 Task.CurrentId로 현재 실행중인 스레드의 아이디를 출력하면 1이 출력되는 것이구요. 스레드를 생성하면서, 상태값으로 1을 넘겨줍니다. 그래서, 그 상태값을 Calc메서드 내부에서 받아서 사용하기도 하고, 추후에 스레드의 상태값을 출력해볼 수도 있는 것이죠.


- 스레드 자세히 들여다 보기

그럼 비주얼 스튜디오에 새로 추가된 툴을 이용해서, 어떤 스레드가 생성되는지 한번 확인해보도록 하겠습니다. 아마, 뒤에서 더 자세하게 설명드리겠지만, 오늘은 그냥 간단하게 살펴보는 정도로 하도록 하죠. <코드1>에 아래와 같이 코드를 추가합니다.

Debugger.Break();

Console.WriteLine("추가 스레드의 ID {0}",
    task.Id);

Console.WriteLine("추가 스레드에 제공된 상태 값 : {0}",
    task.AsyncState.ToString());
           
while (!task.IsCompleted)
{
    if (task.Status == TaskStatus.Running)
    {
        Debugger.Break();
        Console.Write(".");
    }
}

<코드2> 수정한 코드

Debugger클래스는 System.Diagnostics에 정의되어 있는데요, 브레이크 포인트를 걸지 않아도, Break메서드가 호출된 곳에서 실행을 멈추고 디버거를 사용할 수 있습니다. 다만, 그냥 Debug모드로 놓고 컴파일하지 않고 실행(Ctrl + F5)을 하면, 계속 에러가 나니 주의하시구요.

우선 F5로 실행을 하신다음에, 첫번째 브레이크 포인트에 걸리면, '디버그'메뉴에서 '창'메뉴로 들어가서 '스레드', '병렬스택', '병렬작업'을 띄웁니다. 그리고 '스레드'를 보면,

<그림1> 스레드 창

다음과 같이 현재 떠있는 스레드의 목록을 볼 수 있습니다. 저 중에, '.NET SystemEvents'라고 된 스레드는 이벤트의 발생을 주시하고 있는 스레드이구요, 'vshost.RunParkingWindow'라고 된 스레드는 비주얼 스튜디오 호스팅 프로세스입니다. 그리고 '주 스레드'는 현재 Main메서드를 실행 중인 스레드를 말하구요. 그리고 ID가 4112라고 된 스레드가 추가로 생성한 스레드입니다. 어째서 그런지 확인을 해볼까요? 병렬 스택 창을 한번 확인해보죠.


<그림2> 병렬 스택 창

위 그림은 병렬 스택 창에서 스레드 그룹의 제목('4개 스레드'라고 쓰인 부분)에 마우스를 올리면 어떤 스레드가 있는지 보여주는 장면입니다. 총 3개의 스레드가 있는데, 앞에서 설명드린 3개의 스레드외에 작업자 스레드가 하나 있는 걸 보실 수 있습니다. 바로 저 스레드가 우리가 추가로 생성한 스레드인거죠. 그런데, 호출 스택에 Main메서드를 실행하는 주 스레드만 한 단계 진행한 걸 볼 수 있는데요, 그렇다면 아직 추가 스레드에 작업이 물린 상태는 아닌 것 같습니다. 그러면, F5키를 눌러서 다음으로 넘어가 볼까요? 그리고 병렬 스택을 보면,


<그림 3> 바뀐 병렬 스택 창

이미 추가 스레드도 작업에 들어간 상태이다 보니, 추가 스레드가 한 단계 더 진행해서, Calc메서드를 실행하고 있다고 나옵니다. 그리고 병렬 작업 창을 한번 볼까요?

<그림 4> 병렬 작업 창

상태는 실행 중(== TaskStatus.Running)이고, 할당된 스레드는 4112번 작업자 스레드라는 걸 확인할 수 있습니다. 이런 디버깅 툴을 잘 활용하면, 복잡한 병렬 프로그래밍을 하는 데 좀 더 편안하게 작업할 수 있겠죠? 이 디버깅 툴에 대해서는 나중에 좀 더 자세하게 설명드리도록 하지용.


- 마무리

앤더스 헬스버그는 C#에 영향을 미치는 3대 트렌드가 선언적, 동시적, 동적 프로그래밍 이라고 했는데요, 선언적 프로그래밍은 이미 LINQ를 통해서 편하게 지원되고, PLINQ로 확장이 되었죠. 그리고 동적 프로그래밍은 dynamic타입과 DLR을 통해서 지원이 되구요. 그리고 동시적 프로그래밍은 TPL과 이런 다양한 툴을 통해서 좀 더 제대로된 지원을 하고 있습니다. 제대로만 쓴다면, 비주얼 스튜디오 2010에서 쫌 편하게 작업할 수 있겠네요. 늘 문제가 되는건 제대로 쓸 줄도 모르면서 불평만 하는 저 같은 양민이져 어헣-_-.


- 참고자료

1. Essential C# 4.0, Mark Michaelis, Addison Wesley
2. http://msdn.microsoft.com/en-us/library/dd554943.aspx
3. http://msdn.microsoft.com/en-us/library/microsoft.win32.systemevents.aspx
4. http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/f8ccec3a-25db-4d3b-a90a-e758f6243356/

저작자 표시 비영리 변경 금지
신고
- 작업해본 적이나 있수?

물론이죠-_-;; 이 나이에 작업해 본 적도 없으면, 마법사 정도가 아니라, 신이 됐겠죠. 어헣. 오늘의 작업은 그 작업은 아니고... 스레드와 관련된 작업입니다. 부디 오해 없으시길 바라고, 작업의 기본은 다른 연예 서적에서 얻으시길.


- Task 시작하기.

지난 포스트에서 했던 예제를 한번 돌아보겠습니다.

using System;
using System.Threading.Tasks;

namespace Exam2
{
    class Program
    {
        static void Main(string[] args)
        {
            const int max = 10000;

            //현재 작업중인 스레드외에 추가로 스레드를 생성
            Task task = new Task(() =>
                {
                    for (int count = 0; count < max; count++)
                    {
                        Console.Write("|");
                    }
                });

            //추가 스레드 시작
            task.Start();

            //현재 작업중인 스레드에서도 반복문 시작
            for (int count = 0; count < max; count++)
            {
                Console.Write("-");
            }

            //혹시 현재 스레드가 빨리 끝나더라도,
            //추가 스레드가 끝날 때 까지 기다리기.           
            task.Wait();
        }
    }
}

<코드1>

Main메서드를 실행하는 스레드 외에 또 하나의 스레드를 추가로 생성해서, 두 개의 스레드로 화면에 다른 문자열을 출력하는 예제였죠. 이 예제를 보면, Task라는 클래스를 사용하고 있습니다. 이 클래스는 닷넷 프레임워크 4.0에 새롭게 추가된 클래스인데요. 기존의 멀티스레드 프로그래밍을 한 단계 높은 추상화를 통해서 프로그래머의 실수를 줄이고, 좀 더 직관적인 코드를 작성할 수 있게 해주는 TPL(Task Parallel Library)에 포함되어서 추가된 클래스입니다. 중심에 있는 클래스라고 볼 수 있죠.

Task클래스는 관리되지 않는 스레드를 한단계 감싸서 추상화를 시킨 클래스입니다. 내부적으로 스레드 풀을 사용하는 데요, 내부적으로는 System.Threading.ThreadPool을 사용해서 요청에 따라 스레드를 새로 생성하거나, 이미 생성된 스레드를 재활용해서 부하를 줄입니다.

새로운 Task가 실행할 동작은 델리게이트를 통해서 명시해주는데요, <코드1>에서 굵게 처리된 부분이 바로 그 부분입니다. 카운트에 따라서 문자열을 출력하는 델리게이트를 생성자에 넘겨주고 있는 거죠. 물론, 이렇게 델리게이트를 넘겨준다고 해서 바로 스레드가 실행되는 건 아닙니다. Start메서드를 통해서 실행을 해줘야만 스레드가 실행되는 것이죠.


- Task가 끝날 때?

그러면, <코드1>은 두개의 스레드가 실행이 되면서 서로 다른 문자열을 번갈아 가면서 출력하겠죠. 여기서 한가지 생각해볼게 있습니다. 콘솔 어플리케이션은 Main메서드의 실행으로 시작하고, Main메서드의 끝과 함께 종료됩니다. 그렇다면, Main메서드의 실행을 맡은 스레드가 종료되었는데, 추가로 생성한 스레드의 작업이 안끝났다면 어떤 일이 벌어질까요?

//현재 작업중인 스레드외에 추가로 스레드를 생성
Task task = new Task(() =>
    {
        for (int count = 0; count < max; count++)
        {
            Console.Write("|");
        }
        Console.WriteLine("추가 스레드 끝");
    });

//추가 스레드 시작
task.Start();

//현재 작업중인 스레드에서도 반복문 시작
for (int count = 0; count < max; count++)
{
    Console.Write("-");               
}
Console.WriteLine("메인 스레드 끝");

//혹시 현재 스레드가 빨리 끝나더라도,
//추가 스레드가 끝날 때 까지 기다리기.           
//task.Wait();

<코드2>

<코드1>을 <코드2>와 같이 수정한 다음에 실행해보죠. 그러면, 둘 중의 어떤 스레드가 빨리 끝날까요? 그건 그때 그때 다릅니다-_- 그래서 아래와 같은 두 경우가 생길 수 있죠.

||||-----||||||||--------------|||||||||||||-------------||||||||||||-----------||||||---------------||||||||||||------------|||||||||||-----------||||||||||||--|||||||||||||-------|||||||||||-------------||||||||||||--------------||||||||||----------------|||---------------||||||||||||||-----||-------------||||||||||||----------------|||||||||||||-------------||||||||||||-------------|||---|------||||||||||||||-----------------|---------------||||||||||||||---------------메인 스레드 끝
|계속하려면 아무 키나 누르십시오 . . .
<결과1> 메인스레드가 먼저 끝나는 경우

|----------------|||||||||||||-----------||||||||||||||---------||||||||||||-----||||||||||||||||---------------||||||||||||||--------------||-----------||||||||------------|||||||||||||||-------------|--------------|||||||-------------|||||---||추가 스레드 끝----------------------------------------------------------------------------------------------------------------------------------------------------------메인스레드 끝
계속하려면 아무 키나 누르십시오 . . .
<결과2> 추가 스레드가 먼저 끝나는 경우

<결과2>는 추가 스레드가 먼저 끝나면서 모든 결과가 출력이 되었지만, <결과1>은 Main메서드의 실행을 맡은 메인 스레드가 먼저 끝나면서 프로그램이 종료되었고, 따라서 추가스레드의 나머지 결과는 날아가 버렸습니다. Wait메서드는 메인 스레드가 먼저 끝나더라도, 추가 스레드가 끝날 때까지 기다리게 하는 역할을 합니다. 그래서 메인 스레드가 빨리 끝나더라도, 항상 추가 스레드의 결과까지 제대로 출력되게 되는 것이죠.

||||||||--------|||||||||||||||--------------|||||||||||||------------|||||||||||||--------||||
||-메인 스레드 끝||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||추가 스레드 끝
계속하려면 아무 키나 누르십시오 . . .
<결과3> Wait메서드를 사용한 경우


- 가는 길은 여러갈래.

앞에서 스레드의 시작은 Start메서드를 통한다고 말씀 드렸지만, 항상 그런 것은 아닙니다. 생성과 동시에 실행을 시킬 수도 있습니다.

Task task = Task.Factory.StartNew(() =>
                {
                    for (int count = 0; count < max; count++)
                    {
                        Console.Write("|");
                    }
                    Console.WriteLine("추가 스레드 끝");
                });
<코드3> 생성과 동시에 스레드 시작

<코드1>의 Task생성 부분을 <코드3>과 같이 수정하고, Start메서드 호출부분을 주석처리하면, 동일한 결과를 얻을 수 있습니다.

그리고 <코드1>에서는 메인스레드가 추가 스레드가 끝날 때까지 기다리기 위해서 Wait메서드를 사용했지만, 다른 방법도 있습니다. 만약에 추가 스레드에 입력된 델리게이트가 결과값을 반환하고, 메인 스레드에서 그 결과값을 사용해야 한다면, 메인 스레드는 추가 스레드의 작업이 끝나서 결과가 나올 때까지 기다립니다. Wait메서드 없이도 말이죠. 어찌보면 당연한 이야기죠 ㅋ

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Exam3
{
    class Program
    {
        static void Main(string[] args)
        {
            Task<string> task = Task.Factory.StartNew<string>(
                () =>
                {
                    long sum = 0;
                    for (long i = 0; i < 100000000; i++)
                    {
                        sum += i;
                    }
                    return sum.ToString();
                });

            foreach (char busySymbol in BusySymbols())
            {
                if (task.IsCompleted)
                {
                    Console.WriteLine('\b');
                    break;
                }
                Console.WriteLine(busySymbol);
            }

            Console.WriteLine();
            //여기서 추가 스레드가 끝날 때 까지 기다린다.
            Console.WriteLine(task.Result);
            System.Diagnostics.Trace.Assert(
                task.IsCompleted);
        }

        private static IEnumerable<char> BusySymbols()
        {
            string busySymbols = @"-\|/-\|/";
            int next = 0;
            while (true)
            {
                yield return busySymbols[next];
                next = (++next) % busySymbols.Length;
                yield return '\b';
            }
        }
    }
}

<코드 4> 결과 기다리기.

<코드4>는 추가 스레드의 결과를 계속 기다리다가, 결과가 나오는 순간, 출력하고 끝납니다.


- 멀티스레드 쉽고만?

이라고 생각하시면 곤란하구요-_-;; 열심히 공부 중인데, 역시 어렵습니다. 다만, Task클래스가 상당히 많은 부분을 간소화 시켜 주기 때문에, 한 층 더 편해진 느낌이랄까요? 오늘 여기까지!


- 참고자료

1. Essential C# 4.0, Mark Michaelis, Addison Wesley
저작자 표시 비영리 변경 금지
신고