Search

'Visual Studio 2010'에 해당되는 글 260건

  1. 2010.07.10 Asynchronous Agents Library – message block 1. ( 인터페이스 )
  2. 2010.07.03 Asynchronous Agents Library – message 전달 함수. 2 ( 수신 )
  3. 2010.07.01 M, V 그리고 C의 각방생활(8) - jQuery와 탭메뉴 그리고 파샬뷰
  4. 2010.06.30 M, V 그리고 C의 각방생활(7) - 함께 즐겨요~ jQuery 4
  5. 2010.06.29 VS2008 과 VS2010 동시에 개발하기 : 테스트 프로젝트가 포함 될 경우 3
  6. 2010.06.28 Welcome to Parallel C#(10) - 이보게, 잠깐 뒤를 돌아보지 않겠나.
  7. 2010.06.27 M, V 그리고 C의 각방생활(6) - 유효성 검사(2) 5
  8. 2010.06.26 Asynchronous Agents Library - message 전달 함수. 1 ( 전송 )
  9. 2010.06.24 VS2008 을 VS2010 에서 동시에 개발하기 3
  10. 2010.06.22 Welcome to Parallel C#(9) - 백지장은 맞들지 말엉.
  11. 2010.06.17 Welcome to Parallel C#(8) - 취소 쉽게 하기.
  12. 2010.06.16 Visual Studio 2010 최신 PDF 자료를 MSDN 에서 다운로드 받으세요
  13. 2010.06.16 6월 1일, REMIX10 세미나 세션 공개
  14. 2010.06.16 [MFC/윈도우 7 멀티터치] #3 : 제스처(gesture)를 이용한 구현(下)
  15. 2010.06.15 [JumpToDX11-13] Tessellation 등장.
  16. 2010.06.14 Hello Windows Azure / Windows Azure Tools for Visual Studio 1.2 출시
  17. 2010.06.14 Welcome to Parallel C#(7) - Excuse me.
  18. 2010.06.13 Asynchronous Agents Library – agent. 2 ( 기능 )
  19. 2010.06.10 Welcome to Parallel C#(6) - To be continue...
  20. 2010.06.08 Welcome to Parallel C#(5) - 병렬작업에서 예외가 생기면 어케...? 1

Asynchronous Agents Library

– message block 1. ( 인터페이스 )

작성자: 임준환( mumbi at daum dot net )

 

시작하는 글

 이전 글까지 Asynchronous Agents Library( 이하, AAL ) 의 일부인 agent 와 message 전달 함수에 대해 알아보았습니다. agent 만 알아도 어느 정도 비 동기 처리를 쉽게 구현할 수 있습니다.

 이번 글에서는 agent 간 소통을 할 수 있는 message block 들에 대해서 알아보겠습니다. message block 을 이용하면 agent 간 데이터 또는 상태 동기화를 할 수 있습니다.

 AAL 은 스레드로부터 안전한 방식으로 구현되었고, 추상화되었습니다. 그래서 agent 객체와 message block 을 이용한 동기화 로직이 직관적이고 쉽게 흐름을 파악할 수 있어 데드락( dead-lock ) 을 방지하기 용이합니다.

 그럼 지금부터 agent 를 이용한 비 동기 처리에 날개를 달아주는 message block 에 대해 알아보도록 하겠습니다.

 

Message 객체

 예전 글부터 message, message 메커니즘, message 전달 함수, message block 등을 언급하면서 항상 message 란 개념을 사용했습니다.

 이 개념은 실제 클래스로 존재합니다. 하지만 단지 message 를 래핑( wrapping ) 할 뿐, 전혀 다른 기능을 가지고 있지 않은 클래스입니다.

 한 가지 기능이 있다면 식별자( id )를 갖는다는 것입니다. message 클래스는 Concurrency Runtime 의 _Runtime_object 클래스를 상속 받습니다. 이 클래스는 Runtime 에 의해 생성될 때 자동으로 id 를 갖게 됩니다. 이 id 를 알아보는 함수는 msg_id() 입니다. 이 메서드의 접근자가 public 으로 되어 있어 message 클래스에서도 사용 가능합니다.

 이 msg_id() 가 반환한 값은 message block 에서 사용되는 runtime_object_identity 형입니다. 몇몇 message block 메서드의 runtime_object_identity 형의 매개변수에 인자로 사용할 수 있습니다.

 사실, 직접 message block 을 구현하지 않는 한, message 클래스는 직접 사용할 경우는 없을 것입니다. 우리는 보내고 받는 데이터를 공급하면 내부적으로 그 데이터를 message 클래스로 래핑하고 message block 내부에서 사용하게 되는 것입니다. 그러므로 크게 신경쓰지 않아도 됩니다.

 

Source 와 target

 Message block 은 크게 두 가지 종류로 나눌 수 있습니다. 하나는 source 이고 다른 하나는 target 입니다.

 Message block 에서의 source 는 message 를 보낼 message block 을 일컫습니다. 마찬가지로 target 은 message 를 받을 message block 을 뜻합니다.

ISource 인터페이스

 Source 는 AAL 의 하나의 개념이지만, 이것을 인터페이스로 추상화 하였습니다. 이것이 ISource 인터페이스입니다.

 그러므로 source 로 쓰일 message block 들은 ISource 인터페이스를 상속하여 구현되었습니다. 만약 직접 source 로 사용될 message block 을 구현하신다면 ISource 인터페이스를 상속해야 합니다.

- ISource 인터페이스의 선언

template<   class _Type>class ISource;

[ 코드1. ISource 인터페이스의 선언 ]

 템플릿 매개변수인 _Type 은 message 로 쓰일 데이터 형( type )입니다. _Type 은 public typedef 인 source_type 으로 사용할 수 있습니다.

 

- ISource 인터페이스의 메서드

virtual void link_target(ITarget<_Type> * _PTarget) = 0;

[ 코드2. ISource::link_target() ]

 link_target() 은 target 인 message block 과 연결합니다. 여기서 연결의 의미는 자동으로 전달된다는 의미로 생각하시면 되겠습니다.

 즉, 이 ISource 를 상속받은 message block 에 link_target() 으로 target message block 을 연결했을 경우, 이 message block 의 message 들은 직접 전달 함수를 사용하지 않아도 자동으로 target message block 으로 전달됩니다.

 연결할 target 은 여러 개일 수 있습니다. 그러나 ISource 를 상속한 message block 의 구현에 따라 첫 번째 target 만 동작할 수도 있습니다. 예로 unbounded_buffer 가 있습니다. unbounded_buffer 는 내부적으로 큐를 구현하고 있어 전달 후, message 가 큐에서 제거되므로 두 번째 연결된 target 이 있더라도 message 를 보낼 수 없습니다.

 매개변수인 _PTarget 은 연결할 target message block 입니다. _PTarget 의 데이터 형인 ITarget 은 target 을 추상화한 인터페이스입니다. 곧 설명하도록 하겠습니다.

 _PTarget 이 NULL 이라면 invalid_argument 예외가 발생합니다.

 

virtual void unlink_target(ITarget<_Type> * _PTarget) = 0;

[ 코드3. ISource::unlink_target() ]

 unlink_target() 은 link_target() 으로 연결된 target 들 중 매개변수인 _PTarget 에 지정된 target 의 연결을 해제합니다.

 _PTarget 이 NULL 이라면 invalid_argument  예외가 발생합니다. 또한 _PTarget 이 연결된 target 들 중에 없다면 아무 것도 하지 않습니다.

 

virtual void unlink_targets() = 0;

[ 코드4. ISource::unlink_targets() ]

 unlink_targets() 는 연결된 모든 target 들의 연결을 해제합니다.

 

virtual message<_Type> * accept(runtime_object_identity _MsgId, ITarget<_Type> * _PTarget) = 0;

[ 코드5. ISource::accept() ]

 accept() 는 target 에서 호출되지만, source 가 제공합니다. source 의 message 를 수락하고, 소유권이 이전됩니다.

 매개변수인 runtime_object_identity 는 message 객체의 msg_id() 로 얻을 수 있습니다. 실제로 runtime_object_identity __int32 를 typedef 한 것이고, Concurrency Runtime 에서 객체를 생성할 때 지정되는 고유의 번호입니다.

 다른 매개변수인 _PTarget 은 message 를 수락하는 target 입니다.

 수락된 message 가 반환됩니다.

 

virtual bool reserve(runtime_object_identity _MsgId, ITarget<_Type> * _PTarget) = 0;

[ 코드6. ISource::reserve() ]

 reserve() 는 message 를 예약합니다. 예약을 성공한 message 는 message 를 얻기 위한 comsume() 이나 예약을 해제 위한 release() 를 호출해야 합니다.

 매개변수는 위의 accept() 와 같습니다.

 예약에 성공한 경우 true 를, 실패한 경우 false 를 반환합니다. 실패할 수 있는 이유는 다양합니다. 이미 예약되었거나, 구현한 message block 의 특징에 따라 예약에 실패할 수 있습니다.

 

virtual message<_Type> * consume(runtime_object_identity _MsgId, ITarget<_Type> * _PTarget) = 0;

[ 코드7. ISource::consume() ]

 consume() 은 위의 accept() 와 비슷합니다.

 다른 점이 있다면 reserve() 를 호출해 true 를 반환했을 때에만 consume() 을 호출해야 합니다. 보통 reserve() 를 호출하지 않았거나 _Ptarget 이 예약된 target 과 다를 경우 bad_target 예외가 발생합니다.

 매개변수는 위의 accept() 와 같습니다.

 

virtual void release(runtime_object_identity _MsgId, ITarget<_Type> * _PTarget) = 0;

[ 코드8. ISource::release() ]

 release() 는 예약된 것을 해제합니다.

 매개변수는 accept() 와 같습니다.

 

virtual void acquire_ref(ITarget<_Type> * _PTarget) = 0;

[ 코드9. ISource::acquire_ref() ]

 acquire_ref() 는 참조 개수를 증가시킵니다. 현재 link_target() 으로 연결된 target 에서 호출됩니다.

 매개변수인 _PTarget 은 link_target() 으로 연결된 target 입니다.

 

virtual void release_ref(ITarget<_Type> * _PTarget) = 0;

[ 코드10. ISource::release_ref() ]

 release_ref() 는 참조 개수를 감소시킵니다. 현재 link_target() 으로 연결된 target 에서 호출됩니다.

매개변수인 _PTarget 은 link_target() 으로 연결된 target 입니다.

 

ITarget 인터페이스

 Target 또한 source 와 마찬가지로 AAL 의 하나의 개념이지만, 이것을 추상화 하였습니다. 이것이 ITarget 인터페이스 입니다.

 Target 으로 사용할 message block 을 구현하신다면 ITarget 인터페이스를 상속해야 합니다.

- ITarget 인터페이스의 선언

template<   class _Type>class ITarget;

[ 코드11. ITarget 인터페이스의 선언 ]

 템플릿 매개변수인 _Type 은 message 로 사용될 데이터 형입니다. _Type 은 public typedef 인 type 으로 사용할 수 있습니다.

 Target 은 필터를 지정할 수 있습니다. 그래서 필터 함수의 시그니처( signature )인 bool ( _Type const & ) 를 typedef std::tr1::function<bool(_Type const&)> filter_method 로 정의되어 있습니다.

- ITarget 인터페이스의 메서드

virtual message_status propagate(message<_Type> * _PMessage, ISource<_Type> * _PSource) = 0;

[ 코드12. ITarget::propagate() ]

 propagate() 는 지정된 source 로부터 해당 message 를 비 동기 방식으로 가져옵니다.

 매개변수인 _PMessage 는 가져올 message 이고, _PSource 는 보내는 message block 입니다. _PMessage 나 _PSource 가 NULL 일 경우, invalid_argument 예외를 발생할 수 있습니다.

 Message 의 전달이 성공 또는 실패 등의 message 상태를 반환합니다.

 

virtual message_status send(message<_Type> * _PMessage, ISource<_Type> * _PSource) = 0;

[ 코드13. ITarget::send() ]

 send() 는 지정된 source 로부터 해당 message 를 동기 방식으로 가져옵니다.

 매개변수는 propagate() 와 같고, 예외 또한 같습니다.

 Message의 생성 이외에 네트워크와 함께 사용할 경우, 데드락( dead lock ) 을 초래할 수 있습니다.

 

virtual void link_source(ISource<_Source_type> * _PSource)

[ 코드14. ITarget::link_source() ]

 link_source() 는 source 의 link_target() 에 대응되는 함수로 지정된 source 를 연결합니다.

 하지만, 이 함수는 target 에서 호출하면 안되고, source 에서 link_target() 와 함께 호출하여 서로 연결되어야 합니다.

 

virtual void unlink_source(ISource<_Source_type> * _PSource)

[ 코드15. ITarget::unlink_target() ]

 unlink_source() 는 unlink_target() 에 대응되는 함수로 지정된 source 와의 연결을 해제합니다.

 하지만, link_source() 와 마찬가지로 target 에서 호출하면 안되고, source 에서 unlink_target()나 unlink_targets() 와 함께 호출하여 서로 연결을 해제해야 합니다.

 

virtual void unlink_sources()

[ 코드16. ITarget::unlink_sources() ]

 unlink_sources() 는 unlink_targets() 에 대응되는 함수로 지정된 source 들과의 연결을 모두 해제합니다.

 

마치는 글

 이번 글에서는 message block 구현 시, 상속해야 할 인터페이스인 ISourceITarget 에 대해서 알아보았습니다.

 실제로 ISourceITarget 의 메서드들을 직접 호출하는 경우는 거의 없으며, message block 내부에서 사용됩니다.

 Message 를 전달하기 위해서는 위 인터페이스들의 메서드들보다 message 전달 함수들을 많이 사용합니다.

 이번에 소개한 인터페이스들의 구현 클래스들에 대해 아직 소개하지 않았고, 이 인터페이스들의 메서드들이 내부적으로 사용되지만 사용자가 직접 호출할 경우가 드물기 때문에 예제를 작성하지 않았습니다.

 다음 글에서 위 인터페이스들을 구현한 구현 클래스들에 대해서 살펴보고 예제를 보도록 하겠습니다.

Asynchronous Agents Library
– message 전달 함수. 2 ( 수신 )

작성자: 임준환( mumbi at daum dot net )

 

Message 수신

 Message 를 message block 에 전송할 수 있듯이, message block 으로부터 수신할 수도 있습니다. message 수신 함수에도 전송 함수와 마찬가지로 동기 함수인 receive() 와 비 동기 함수인 try_receive() 가 있습니다.

 

동기 함수 receive()

 동기 함수인 receive() 는 message block 으로부터 수신이 완료될 때 수신된 message 를 반환합니다. 만약 message block 에 어떠한 message 도 없다면 receive() 는 message block 에 수신할 message 가 있을 때까지 기다립니다.

 아래는 receive() 의 선언입니다.

template <
   class _Type
>
_Type receive(
   ISource<_Type> * _Src,
   unsigned int _Timeout = COOPERATIVE_TIMEOUT_INFINITE
);

template <
   class _Type
>
_Type receive(
   ISource<_Type> * _Src,
   filter_method const& _Filter_proc,
   unsigned int _Timeout = COOPERATIVE_TIMEOUT_INFINITE
);

template <
   class _Type
>
_Type receive(
   ISource<_Type> &_Src,
   unsigned int _Timeout = COOPERATIVE_TIMEOUT_INFINITE
);

template <
   class _Type
>
_Type receive(
   ISource<_Type> &_Src,
   filter_method const& _Filter_proc,
   unsigned int _Timeout = COOPERATIVE_TIMEOUT_INFINITE
);

[ 코드1. receive() 의 선언 ] 

템플릿 매개변수인 _Type 은 message 의 자료 형입니다.

 함수 매개변수 중 _Src 는 message block 의 인터페이스 중 하나인 ISource 를 상속한 message block 객체이며, 이 객체로부터 message 를 수신합니다.

 함수 매개변수 중 _Timeout 은 최대 대기 시간입니다. 이것은 receive() 가 동기 함수이기 때문에 영원히 기다릴 상황을 대비하는 방법입니다. 이 매개변수를 지정했을 때, 최대 대기 시간을 초과하였을 경우, agent::wait()( Asynchronous Agents Library – agent. 2 ( 기능 ) 참고 ) 와 마찬가지로 operation_timed_out 예외를 발생합니다. 그러므로 이 매개변수를 지정 시 반드시 해당 예외를 처리해주어야 합니다. 기본 인자로 COOPERATIVE_TIME_INFINITE 가 지정되어 있으며, 무한히 기다리는 것을 의미합니다.

 함수 매개변수 중 _Filter_proc 는 message 를 거부할 수 있는 필터입니다. message block 생성자로 지정할 수 있는 필터와 마찬가지로 std::tr1::function<bool(_Type const&)> 입니다.

 Message 의 수신이 완료되면 해당 message 를 반환합니다.

예제

- 수신할 message 가 있는 경우

#include <iostream>
#include <string>
#include <agents.h>

using namespace std;
using namespace Concurrency;

int main()
{
	// message block
	unbounded_buffer< wstring > message_block;

	wstring send_message( L"first message!" );
	send( message_block, send_message );

	wstring receive_message;

	try
	{
		receive_message = receive( message_block, 2000 );
	}
	catch( operation_timed_out& e )
	{
		wcout << L"operation_timed_out exception" << e.what() << endl;
	}

	wcout << L"received message: " << receive_message << endl;
}

[ 코드2. receive() 의 수신할 message 가 있는 경우 예제 ]

 receive() 의 매개변수로 최대 대기 시간을 지정했지만 message block 에 message 가 존재하기 때문에 수신하고 바로 반환합니다.

[ 그림1. receive() 의 수신할 message 가 있는 경우 예제 실행 결과 ]

[ 그림1. receive() 의 수신할 message 가 있는 경우 예제 실행 결과 ]


- 수신할 message 가 없는 경우

#include <iostream>
#include <string>
#include <agents.h>

using namespace std;
using namespace Concurrency;

int main()
{
	// message block
	unbounded_buffer< wstring > message_block;

	// wstring send_message( L"first message!" );
	// send( message_block, send_message );

	wstring receive_message;

	try
	{
		receive_message = receive( message_block, 2000 );
	}
	catch( operation_timed_out& e )
	{
		wcout << L"operation_timed_out exception" << e.what() << endl;
	}

	wcout << L"received message: " << receive_message << endl;
}

[ 코드3. receive() 의 수신할 message 가 없는 경우 예제 ]

 send() 를 주석 처리하여 message block 에 전달한 message 가 없기 때문에 receive() 는 지정된 최대 대기 시간인 2초( 2000 milli second ) 동안 기다린 후, operation_timed_out 예외가 발생합니다.

 이 예외를 처리해야 정상적으로 프로그램이 진행됩니다.

 만약 최대 대기 시간을 지정하지 않았다면 무한 대기하게 됩니다.

[ 그림2. receive() 의 수신할 message 가 없는 경우 예제 실행 결과 ]

[ 그림2. receive() 의 수신할 message 가 없는 경우 예제 실행 결과 ]


 

비 동기 함수 try_receive()

비 동기 함수인 try_receive() 는 message 가 수신될 때까지 기다리지 않습니다. 만약 수신할 message block 에 어떠한 message 도 없다고 하더라도 기다리지 않고, 바로 반환됩니다.

아래는 try_receive() 의 선언입니다.

template <
   class _Type
>
bool try_receive(
   ISource<_Type> * _Src,
      _Type & _value
);

template <
   class _Type
>
bool try_receive(
   ISource<_Type> * _Src,
      _Type & _value,
   filter_method const& _Filter_proc
);

template <
   class _Type
>
bool try_receive(
   ISource<_Type> & _Src,
      _Type & _value
);

template <
   class _Type
>
bool try_receive(
   ISource<_Type> & _Src,
      _Type & _value,
   filter_method const& _Filter_proc
);

[ 코드4. try_receive() 의 선언 ]

 템플릿 매개변수인 _Type 은 receive() 와 마찬가지로 message 의 자료 형입니다.

 함수 매개변수 중 _Src 도 receive() 와 마찬가지로 message block 의 인터페이스 중 하나인 ISource 를 상속한 message block 객체이며, 이 객체로부터 message 를 수신합니다.

 함수 매개변수 중 _value 는 수신한 message 를 저장할 변수의 참조입니다. 수신이 성공하면 message 는 이 참조가 가리키는 변수에 저장됩니다.

 함수 매개변수 중 _Filter_proc 는 receive() 와 마찬가지로 message 를 거부할 수 있는 필터입니다.

 try_receive() 는 수신의 완료를 기다리지 않기 때문에 수신을 시도했을 때( try_receive() 를 호출했을 때 ) message block 에 어떠한 message 도 없다면 false 를 반환해 알려줍니다. message 가 있다면 true 를 반환합니다.

 만약, 수신 시도를 하자마자 시도한 컨텍스트가 계속 진행되기를 원한다면 receive() 에 _Timeout 매개변수에 0 을 지정하기 보다는 try_receive() 를 사용하는게 바람직합니다.

예제

- 수신할 message 가 있는 경우

#include <iostream>
#include <string>
#include <agents.h>

using namespace std;
using namespace Concurrency;

int main()
{
	// message block
	unbounded_buffer< wstring > message_block;

	send( message_block, wstring( L"first message!" ) );

	wstring received_message;
	if( try_receive( message_block, received_message ) )
	{
		wcout << L"receive success" << endl;
		wcout << L"received message: " << received_message << endl;
	}
	else
	{
		wcout << L"receive fail" << endl;
	}
}

[ 코드5. try_receive() 의 수신할 message 가 있는 경우 예제 ]

 try_receive() 는 비 동기 함수이기 때문에 수신할 message 가 있든 없든 먼저 반환됩니다. 수신할 message 가 있으면 true 를 반환하고 인자인 참조 변수에 수신한 message 를 저장합니다,

[ 그림3. try_receive() 의 수신할 message 가 있는 경우 예제 실행 결과 ]

[ 그림3. try_receive() 의 수신할 message 가 있는 경우 예제 실행 결과 ]


- 수신할 message 가 없는 경우

#include <iostream>
#include <string>
#include <agents.h>

using namespace std;
using namespace Concurrency;

int main()
{
	// message block
	unbounded_buffer< wstring > message_block;

	// send( message_block, wstring( L"first message!" ) );

	wstring received_message;
	if( try_receive( message_block, received_message ) )
	{
		wcout << L"receive success" << endl;
		wcout << L"received message: " << received_message << endl;
	}
	else
	{
		wcout << L"receive fail" << endl;
	}
}

[ 코드6. try_receive() 의 수신할 message 가 없는 경우 예제 ]

 동기 함수인 receive() 와는 달리 비 동기 함수인 try_receive() 는 수신할 message 가 없을 경우, 기다리지 않고 false 를 반환합니다.

 수신할 message 가 있든 없든 바로 반환해야 하는 경우라면 receive() 의 매개변수인 최대 대기 시간을 0 으로 지정하는 것보다는 try_receive() 를 권장합니다.

 receive() 의 최대 대기 시간은 예외 메커니즘을 사용하므로 try_receive() 에 비해 오버헤드가 있을 수 있습니다.

[ 그림4. try_receive() 의 수신할 message 가 없는 경우 예제 실행 결과 ]

[ 그림4. try_receive() 의 수신할 message 가 없는 경우 예제 실행 결과 ]


 

Message 필터

 지난 글에서 message block 에 필터를 지정할 수 있다고 언급했습니다. message block 에 지정되는 필터는 message block 에 전송 시에 적용됩니다.

 마찬가지로 수신 함수들에도 필터를 지정할 수 있습니다. message block 으로부터 수신 시에 적용됩니다.

 동기 함수인 receive() 는 수신할 message 가 필터에 의해 수락될 때까지 대기합니다. 즉, 수신할 message 가 필터에 의해 거부된다면 message block 에 message 가 없을 때와 같습니다.

 비 동기 함수인 try_receive() 또한 message block 에 message 가 없을 때와 마찬가지로 false 를 반환합니다.

 다시 말해, message block 에 message 가 없는 경우에도 필터에 의해 거부된다는 말과 같습니다.

 그럼, 필터를 이용한 수신 함수에 대한 예제를 살펴보겠습니다.

예제

#include <iostream>
#include <vector>
#include <iterator>
#include <functional>
#include <agents.h>
#include <ppl.h>

using namespace std;
using namespace std::tr1;
using namespace Concurrency;

class number_collector
	: public agent
{	
public:
	number_collector( ISource< int >& source, vector< int >& result, function< bool ( int ) > filter )
		: source( source )
		, result( result )
		, filter( filter ) { }
	
protected:
	void run()
	{
		while( true )
		{
			int number = receive( this->source, this->filter );

			if( 0 == number )
				break;

			this->result.push_back( number );
		}

		this->done();
	}

private:
	ISource< int >&				source;
	vector< int >&				result;
	function< bool ( int ) >	filter;
};

int main()
{
	// message block
	unbounded_buffer< int > message_block;

	// send number 1 ~ 10.
	parallel_for( 1, 11, [&]( int number )
	{
		send( message_block, number );
	} );

	// send stop signal.
	send( message_block, 0 );	// for even.
	send( message_block, 0 );	// for odd.

	vector< int > even_number_array, odd_number_array;

	number_collector even_number_collector( message_block, even_number_array, []( int number ) -> bool
	{
		return 0 == number % 2;
	} );

	number_collector odd_number_collector( message_block, odd_number_array, []( int number ) -> bool
	{
		if( 0 == number )
			return true;

		return 0 != number % 2;
	} );

	even_number_collector.start();
	odd_number_collector.start();

	// wait for all agents.
	agent* number_collectors[2] = { &even_number_collector, &odd_number_collector };
	agent::wait_for_all( 2, number_collectors );

	// print
	wcout << L"odd numbers: ";
	copy( odd_number_array.begin(), odd_number_array.end(), ostream_iterator< int, wchar_t >( wcout, L" " ) );

	wcout << endl << L"even numbers: ";
	copy( even_number_array.begin(), even_number_array.end(), ostream_iterator< int, wchar_t >( wcout, L" " ) );

	wcout << endl;
}

[ 코드7. 필터를 이용한 숫자 고르기 예제 ]

 우선 message block 에 1 ~ 10 의 정수를 전송합니다. parallel_for() 를 사용하였는데 이 함수는 Concurrency Runtime 위에서 AAL 과 작동하는 돌아가는 Parallel Patterns Library( 이하, PPL ) 에서 제공하는 함수입니다. PPL 에 대한 자세항 사항은 visual studio 팀 블로그에서 확인하실 수 있습니다.

 parallel_for() 는 반복될 내용을 병렬로 처리하기 때문에 성능에 도움을 줍니다. 그러나 반복되는 순서를 보장하지 않습니다.

 그래서 1 ~ 10 의 정수가 전송되는 순서는 알 수 없습니다. 하지만 1 ~ 10 의 정수를 모두 전송한 뒤, 0을 보내서 마지막 message 라는 것을 알려주었습니다. 두 번 보낸 0 중 하나는 짝수를 수신하는 agent 를 위한 것이고, 하나는 홀수를 수신하는 agent 를 위한 것입니다.

 사실, 이런 처리 로직을 구성할 때에는 상태 변화 알림에 유용한 다른 message block 을 사용하는 것이 좋지만 아직 message block 에 대해서 설명하지 않았기 때문에 혼란을 줄이기 위해 간단한 unbounded_buffer 하나만으로 처리하였습니다.

 위 코드에 정의된 agent 인 number_collector 는 message block 으로부터 필터에 의해 필터링된 message 를 컨테이너에 저장합니다.

 동기 함수인 receive() 를 사용했기 때문에 원하는 message 가 올 때까지 기다립니다. 이로 인해 필요한 만큼의 최소의 반복을 하여 오버헤드가 줄어 듭니다.

 만약 비 동기 함수인 try_receive() 를 사용했다면 쓸모 없는 반복 오버헤드를 발생시킬 것입니다. 이 예제의 경우에는 동기 함수인 receive() 가 적합합니다.

 정의된 agent 를 짝수용과 홀수용을 선언하고 start() 를 사용하여 작업을 시작합니다. 그리고 wait_for_all() 을 사용하여 두 agent 가 모두 끝날 때까지 기다린 후, 모든 작업이 종료되면 화면에 수집한 정수들을 출력합니다.

  위 예제 코드는 Visual studio 2008 부터 지원하는 tr1function 과 visual studio 2010 부터 지원하는 C++0x 의 람다를 사용하였습니다. Concurrency Runtimetr1, C++0x 등의 visual studio 2010 의 새로운 feature 들을 사용하여 구현되었기 때문에 이것들에 대해 알아두는 것이 좋습니다.

[ 그림5. 필터를 이용한 숫자 고르기 예제 실행 결과 ]

[ 그림5. 필터를 이용한 숫자 고르기 예제 실행 결과 ]


 

마치는 글

 이 글에서는 message 전달 함수 중 수신 함수인 receive() 와 try_receive() 에 대해서 알아보았습니다.

 receive() 와 try_receive() 는 사용해야 할 상황이 분명히 다르니 상황에 따라 사용에 유의해야 합니다.

 다음 글에서는 message 가 저장되는 message block 에 대해서 알아보도록 하겠습니다.

안녕하세요. 늦바람이 무섭다고 하는데요. jQuery를 향한 늦바람이 불어주길 바라는 1인입니다. ㅎㅎ

이렇게 간단해도 되는겨?

이번 포스팅을 준비하면서 정말 jQuery의 놀라운 힘에 다시 한번 놀랐습니다. 이렇게 간단히 탭메뉴를 넣는게 가능했던건가요?

준비물 준비

먼저, jQueryUI 사이트에서  jquery-ui-1.8.2.custom.zip 파일을 다운받습니다. 압축을 푸시면 jquery-ui-1.8.2.custom.min.js 와 jquery-ui-1.8.2.custom.css 파일이 있습니다.(각각 js폴더와 css폴더에 있습니다.) 이 두 파일을 프로젝트의 Content와 Scripts 폴더에 추가시킵니다. 이제 준비는 됐고요.

준비끝! 예제로!

Index.aspx 페이지 소스입니다.

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    홈 페이지
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <script type="text/javascript">
        $(function () {
            $("#tabs").tabs();
        });
    </script>
    <div>
        <div id="tabs">
        <ul>
            <li><%: Html.ActionLink("홈", "Index", "Product")%></li>
            <li><%: Html.ActionLink("제품", "List", "Product")%></li>
            <li><%: Html.ActionLink("연락", "Contact", "Product")%></li>
        </ul>
        </div>
    </div>
</asp:Content>

$("#tabs").tabs() 이게 바로 그 놀라운 능력을 가진 탭메뉴를 가능케하는 힘입니다. (자세한 것은 다음으로 미루고~ 언제가 될지는 몰라요. 그냥 잘 쓰면 되는거죠^^;;)
아. 추가시켰던 두 파일은 마스터페이지에 끌어다놨습니다.


이제 Html.ActionLink() 에 걸린 액션들만 만들어 주면 됩니다.


요청 컨트롤러입니다. 첫 Index() 액션 메쏘드를 보시면 dynamic 이라는 타입이 보이는데요. 처음에는 Contact()와 마찬가지로 PartialView()만 리턴을 하였는데, 파샬뷰를 생성해 놓고 보니.


저렇게 dynamic 이 눈에 딱 띄는바람에 어쩔수(?) 없이 dynamic데이터를 전달하게 되었습니다. 간단히 설명드리면 dynamic은 대인배의 마음 씀씀이를 갖고 있어서 어떤 타입이던지 모두 수용합니다.(컴파일타임에는 터치를 안합니다. 귀찮아서 런타임한테 넘기는거죠;;) dynamic으로 선언된 변수에 멤버, 메쏘드, string, int 가리지 말고 막 넣어주세요. 다 받아줍니다. 하.. 저도 대인배로 살아가야 할텐데 참.. 아쉽습니다 :)
다음 기회에 dynamic에 대해 좀더 자세히 알아보면 좋겠네요.

다시 본론으로 넘어와서, /Product/Index.ascx 를 보시면

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<dynamic>" %>
<h3><%: Model.Message %></h3>
<p>
    ASP.NET MVC에 대한 자세한 내용을 보려면 <a href="http://asp.net/mvc" title="ASP.NET MVC 웹 사이트">http://asp.net/mvc</a>를 방문하십시오.
</p>

Model객체의 Message의 접근하면(Model.Message) Index 메쏘드에서 넘겨준 다이나믹한 메시지를 받을 수 있습니다.

두번째, /Product/List.ascx는

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IEnumerable<MvcWithjQuery.Models.Product>>" %>
<table width="400px">
<tr>
    <th>제품명</th>
    <th>가격</th>
</tr>
<% foreach (var product in Model) { %>
<tr>
    <td><%: product.Name %></td>
    <td><%: product.Price %></td>         
</tr>
<% } %>
</table>

너무 간단해서 할말을 잃게 만들죠. foreach문을 통해 루프를 돌면서 데이터를 출력합니다.
나머지 Contact.ascx는 안보셔도 됩니다.^^;

자, 완료가 되었으니 확인을 해보죠.


페이지 로드 없이 깔끔하게 탭기능이 완성되었습니다.

마무리요

일반적인 페이지(aspx)도 탭메뉴로 가능합니다. 파샬뷰를 사용한 것은 한 페이지 전체보다 불필요한 부분을 제거한(head, html, body가 보시다시피 파샬뷰에는 존재하지 않습니다.) 간결함때문이랄까요? ㅎㅎ
다음은 더 재미있는 것으로 찾아뵙겠습니다. (지금은 재밌다는겨? 뭐여? ㅡ.ㅡ 이렇게 생각하시는 분은 제발 없으시길 바래요^^)

참고자료 :  http://www.kevgriffin.com/blog/index.php/2010/02/23/using-jquery-tabs-and-asp-net-mvc-partial-views-for-ajax-goodness/
지금 잠을 자면 꿈을 꿀 수 있지만, 잠을 자지 않으면 꿈을 이룰 수 있다고 하죠. 그래서 이렇게 눈꺼풀이 내려오는데도 버티고 있는가 봅니다^^; 이 글을 읽고 있는 분들도 꿈을 위해 노력하고 계신거겠죠?

귀 따갑다 jQuery

고마해라~ 마이 들었다 아이가~. 너무 들어서 지겨울 만큼의 jQuery. 이제 시작합니다. 이렇게 늦게 jQuery를 들쳐보는 저를 용서하시고, 격려의 한말씀 해주시면 정말 감사하겠습니다. :)
그런데, jQuery 정말 다들 아시는거죠?

jQuery가 뭔데?

아직도 jQuery를 모른단 말인가?! 자네 정말 웹 관련 일하는 사람이 맞긴 맞는건가? 어헣. 이런 말이 오고가진 않아야 겠죠?

jQuery 공식 홈페이지에는 다음과 같은 말이 떡~하니 있습니다.

jQuery is a new kind of JavaScript Library.
jQuery is a fast and concise JavaScript Library that simplifies HTML document traversing, event handling, animating, and Ajax interactions for rapid web development. jQuery is designed to change the way that you write JavaScript.

쭉 보면,
jQuery는 자바스크립트 라이브러리의 한 종류입니다. jQuery는 신속한 웹 개발을 위한 HTML 문서 탐색, 이벤트 처리, 애니메이션, Ajax와의 상호작용을 간단하게하는 빠르고 간결한 자바스크립트 라이브러리입니다. jQuery는 자바스크립트 작성 방식의 전환을 위해 설계되었습니다.

다른건 몰라도, 암튼 웹 개발을 빠르게 해준다니까 오케이입니다. 귀찮은 작업도 간결하게 해주는 것 같고요.
Visual Studio 에 jQuery가 탑재되어있는 것도 마이크로소프트가 이를 지원한다는 얘기? 그래서 오케이. 스캇 구쓰리의 블로그를 보시면 계속 jQuery 플러그인 얘기가 올라오고 있는 것으로 봐서 활발하게 개발중인 것 같습니다.

간단 예제

정말 간단한 예제를 한번 살펴보도록 하겠습니다. 실망하시면 안~되요.
지금 하려는 것은 'ASP.NET MVC에 대한 자세한 내용을 보려면...' 이 있는 p 태그의 스타일을 변경해 볼겁니다. 먼저 MVC 프로젝트를 새로 생성하겠습니다. 그 다음, Index.aspx 페이지에 Contents 폴더에 있는 jquery-1.4.1.js 파일을 끌어다 놓습니다. '$(' 입력해보시면 놀랍게도 정말 놀~랍게도 인텔리센스를 지원해주는 것을 확인하실 수 있습니다. 멋지죠? :-)


아. 이거할때가 아닌데 좋아하고 있었네요. 먼저 필요한 스타일을 추가하겠습니다. 간단합니다.


색과 크기변경만 할겁니다.^^;
그 다음 스타일을 변경해줄 소스를 추가하겠습니다.

    <script src="../../Scripts/jquery-1.4.1.js" type="text/javascript"></script>
    <script type="text/javascript">
        $(function () {           
            $('#btnStyle1').click(function () {
                $('p').removeClass();
                $('p').addClass('color-yellow').addClass('size-large');
            });
            $('#btnStyle2').click(function () {
                $('p').removeClass();
                $('p').addClass('color-red').addClass('size-small');
            });
        });
    </script>
    <div id="styleChange" style="background-color:Gray; width:300px">
        <h3 style="color: Yellow">스타일을 바꿔요</h3>
        <button id="btnStyle1">스타일_1</button>
        <button id="btnStyle2">스타일_2</button>
    </div>

jQuery 의 경우, 메쏘드 체인이 가능합니다. $().addClass().addClass().removeClass()...
암튼, 너무 간단해서 할 말을 잃으셨다면, 다음을 기대(?)해주세요^^;
실행 결과를 보면 스타일_1 버튼을 클릭했을시,


스타일_2 버튼을 클릭했을시,


참~ 이쁘게(?) 되네요.

마무리요

간단해서 따로 드릴 말씀은 없고요. 바로 다음 글 준비하겠습니다!!!


참고자료 : jQuery 1.3, 조나단 채퍼, 칼 스웨드버그

지난 아티클에서 Visual Studio 2008 과 Visual Studio 2010 을 동일한 소스 코드와 프로젝트로 개발하기 위한 환경을 구성하는 방법을 알아보았습니다.

문제 원인

하지만 지난 시간에 언급한 듯이 테스트 프로젝트가 포함된 경우는 VS2008 과 VS2010 을 동시에 사용할 수 없는 문제가 발생합니다. 그 이유는 Microsoft.VisualStudio.Quality 프레임워크가 개선이 되고, Microsoft.VisualStudio.TestTools 프레임워크가 도입되면서 기존의 테스트 프레임워크와 비호환적인 부분이 존재하게 됩니다.

아래와 같이 VS2008 에서 작업한 테스트 프로젝트가 있을 경우,

지난 아티클의 방법으로 프로젝트를 변환하게 되면 아래의 오류가 발생합니다.

기존 프로젝트는 기존의 .NET Framework 버전을 그대로 사용할 수 있지만, 테스트 프로젝트는 반드시 .NET Framework 4.0 으로만 사용할 수 있습니다.

만약 하위 프레임워크 버전인 .NET Framework 3.5 로 변경하고자 할 경우 아래와 같은 오류 메시지가 나타나고, 다시 .NET Framework 4.0 버전으로 변경이 됩니다.

일부 이미 완성된 테스트 프로젝트인 경우 이것보다 더 다양한 오류 메시지를 볼 수 있습니다.^^;

어쨌든, Microsoft.VisualStudio.QualiltyTools 프레임워크가 .NET Framework 4.0 버전으로 고정되어 자칫 테스트 프로젝트가 굉장히 큰 우범을 저지를 수 있는 문제가 될 수 있습니다.

반대로 Visual Studio 2010 에서 만든 테스트 프로젝트는 .NET Framework 4.0 이 기본이고 Microsoft.VisualStudio.QualityTools 프레임워크와 Microsoft.VisualStudio.TestTools 프레임워크가 기본 참조(RTM 에서는 제외 됨)이며, 이 프레임워크의 버전도 4.0.xxxxx 버전이라 하위 버전과 호환되지 않습니다.

VS2010 이든 VS2008 이든 테스트 프로젝트가 한번 VS2010 으로 업그레이드 되었다면, VS2008 에서 테스트 프로젝트를 사용하기 위해서는 다운그레이드는 그리 쉽지 만을 않습니다. 왜냐하면 VS2008 에서 테스트 프로젝트를 로드하면 프로젝트의 참조가 깨져있습니다.

그 이유는 .csproj 파일을 열어보면 답이 나오는데요. 아예 Microsoft.VisualStudio.QualityTools 프레임워크의 버전 번호를 명시하여 해당 VS2008 에서는 어셈블리 리디렉션(Assembly Redirection) 을 시키기가 좀 애매해 집니다.

   

문제 해결 기본 지식

이 문제를 해결하기 위해서는 테스트 프로젝트간의 비 호환적인 테스트 프레임워크로 인한 문제이므로, 문제를 해결하기 위한 접근 측면에 제한을 둘 수 밖에 없습니다. Visual Studio 2010 에서는 Coded UI, Test Impact 등 새로운 기능이 추가되었고, 기존 테스트 또한 비주얼한 부분이 개선이 되면서 강제적으로 테스트 프레임워크의 버전을 .NET Framework 4 로 고정을 시키는 것 같습니다.

이 문제는 MSBuild 를 통해 해결하기 위한 기본적인 지식을 알려드립니다. 여러분이 알다시피 MSBuild 는 Microsoft 의 통합 빌드 솔루션입니다. 예전에는 "빌드"라는 말 대신 "컴파일"이라는 단어를 사용했었죠. 컴파일이란 소스 코드를 목적 파일 또는 실행 파일로 변환하는 과정을 "컴파일"이라고 합니다.

컴파일과 빌드를 비교하는 아주 간단한 그림 입니다.
컴파일은 목적은 소스 코드를 목적 파일로 변환하여 실행 파일 또는 라이브러리로 만들기 위한 목적입니다.

빌드는 컴파일의 일련의 과정을 플로우(Flow) 로 처리하여 컴파일 중에 더 많은 작업을 하기 위한 목적입니다.
가장 대표적인 빌드 솔루션이 MSBuild 이며, 이 외에 Ant 또는 NAnt 등이 바로 이러한 솔루션입니다. 그리고 Team Foundation Server 의 팀 빌드도 바로 MSBuild 에 기반하고 있다는 것입니다.

필자 또한 MSBuild 를 접하면서 나의 지식의 끝을 무한하게 확장해 주었던 것이 MSBuild 입니다. MSBuild 는 정적인 컴파일 방식에서 동적인 방식의 빌드로 거듭나면서 굉장히 많은 가능성을 보여주는 부분이기도 합니다. Microsoft 의 MSBuild 의 대략적인 구조는 아래와 같습니다.

기본적으로 MSBuild 는 Task 의 집합이라고 해도 과언이 아닙니다. 그리고 이 Task 중에 빌드와 연관된 Task 도 있습니다. 이 Task 를 .NET Framework 버전에 따라 Project References(프로젝트 참조)를 변형시키는 방법입니다.

 

해결 방법

이 테스트 프로젝트를 VS2008, VS2010 양 쪽에서 사용하도록 하기 위해서는 이 어셈블리 참조를 동적으로 변화시킬 필요가 있습니다. 이 방법도 MSBuild 의 Choose 라는 조건문으로 제어를 분기할 수 있는 방법입니다.

1. 먼저 솔루션 탐색기에서 열려 있는 프로젝트를 언로드 한 후, 편집을 클릭합니다.

2. 그럼 아래와 같이 참조와 관련되어 있는 부분이 ItemGroup 요소에 있는 것을 확인할 수 있습니다.

3. 이 ItemGroup 에서 VS2008, VS2010 에서 공통적인 참조 어셈블리를 별도의 ItemGroup 으로 분리합니다. 그럼 아래와 같은 형태가 되겠지요?

4. 테스트와 관련된 ItemGroup 에 Choose 조건 분기 요소를 사용하여 조금 변형해 봅시다. .NET Framework 의 버전 별로 말이죠.

위의 $(MSBuildBinPath) 는 실제로 빌드가 수행할 때의 MSBuild 의 경로를 나타냅니다. 하지만 여기에는 한 가지 함정이 있습니다. Visual Studio 2008 에서는 <Message Text="$(MSBuildBinPath)" /> 가 아래와 같이 C:\Windows\Microsoft.NET\Framework\v2.0.50727 로 나타납니다. 하지만 내부적으로 이 MSBuild 는 v3.5 경로의 MSBuild.exe 를 실행하게 됩니다. 자세한 이유와 내막은 Microsoft.Common.targets 파일을 뒤져보시면 아실거라고 생각합니다.

그리고 Choose 조건 분기 요소는 if ~ else 와 같은 구문입니다. ItemGroup 요소는 하나의 항목을 담는 필드라고 보시면 되고, PropertyGroup 은 한 Property 에 여러 항목을 담는 속성이라고 보시면 됩니다. 이 부분은 MSBuild 를 공부해 보시면 어렵지 않는 기본적인 부분이니 자세한 설명은 여기에서 하지 않겠습니다.

5. 모두 완료 되었습니다. 각각의 VS2008, VS2010 에서 테스트 프로젝트를 모두 사용할 수 있게 되었습니다.

만약 Coded UI 와 같은 VS2010 의 새로운 기능을 사용할 경우 아래와 같이 추가적인 어셈블리를 참조하게 됩니다.

이 경우도 위의 4번과 같이 ItemGroup 의 VS2010 용 어셈블리를 아래와 같이 넣어버리면 됩니다.

그럼 VS2008 인지 VS2010 인지에 따라서 참조 어셈블리가 완벽하게 분리가 됩니다.

하지만 VS2008 에서 빌드를 할 경우 아래와 같이 오류가 발생하게 됩니다. 당연히 VS2008 에서는 Coded UI 등에서 필요한 Microsoft.VisualStudio.TestTools 프레임워크가 존재하지 않고, 이 프레임워크를 재사용하기 힘들기 때문입니다.

하지만 이 문제로 해결해 볼까요? 위에서 Property 를 재정의한 구문이 생각나실 겁니다. 전처리 지시문의 상수 값으로 사용되는 <DefineConstants> 에 NET4.0 빌드인지, NET3.5 빌드인지 알 수 있도록 상수 값을 선언하였습니다.

이 상수 값을 이용하여 CodedUI 등 VS2010 에서 새로 추가된 부분에, #if ~ #endif 지시문을 사용하여 감싸 주시면 됩니다.

   

이제 Visual Studio 2008 이든 Visual Studio 2010 이든 테스트 프로젝트를 양 쪽 어떤 도구를 사용하든 테스트가 가능하도록 구성하는 방법을 완료하였습니다.

- 뜬금없이 뭐여..?

지금까지는 닷넷 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

M, V 그리고 C의 각방생활(6) - 유효성 검사(2)

ASP.NET MVC 2010. 6. 27. 09:00 Posted by 네버덜레스
유효성 검사 안끝난겨?

네. 아직입니다. ^^; 원래는 마무리를 지으려고 했었는데요. 갑자기 jQuery 가 급땡기는 바람에 슬슬 관련글을 적어보렵니다.

클라이언트단에서 유효성검사하기

지난번 포스팅을 보시면, 서버단의 모델 클래스에 DataAnnotaion을 사용하여 유효성검사를 했습니다. 물론, 클라이언트단에서도 자바스크립트를 사용하여 유효성검사를 할 수 있지만, 이는 동일한 유효성 검사를 두번(서버와 클라이언트) 하게됩니다. DRY(Don't Repeat Yourself) 규칙에 위반되는 작업인 거죠.

근데 왜?

저 아시는 분 없죠? 듣보잡인거죠. 그래서 이렇게 앞뒤가 없습니다. 이번 포스팅을 먼저 했으면 하는 마음도 있지만, 뭐 이렇게 된 것 그냥 적어내려갑니다.^^
DRY에 반하는 작업을 한다고 너무 차가운 피드백은 달지 말아주세요; '이런 방법도 있는 거였군'이라는 생각만 가져주셨으면 좋겠습니다.

먼저, 지난번 유효성 검사를 했던 소스에 jQuery를 이용한 유효성 검사 스크립트를 추가하겠습니다.


프로젝트내의 Scripts폴더에 있는 jquery-1.4.1.js와 jquery.validate.js파일을 추가합니다.(프로젝트에 이런 스크립트 파일들이 자동으로 적용되어있는 것으로 봐서는 맘껏 사용하라는 거겠죠?^^; 아. 그리고 미니버전을 사용해도 되는 것은  다들 아시죠? *min.js)

추가된 소스도 함께 보시죠.

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<dynamic>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
 Create
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <script src="/Scripts/jquery-1.4.1.js" type="text/javascript"></script>
    <script src="/Scripts/jquery.validate.js" type="text/javascript"></script>   
    <script type="text/javascript">
        $(function () {
            $("form").validate({               
                rules: {
                    "Name": { required: true, maxlength: 5 },
                    "Phone": { required: true },
                    "SpeedDial": { required: true, range: [1, 99] },
                    "Email": { email: true }
                },
                messages: {
                    "Name": "5자 이내로 이름을 입력하시오.",
                    "Phone": " 전화번호를 입력하시오.",
                    "SpeedDial": "1~99까지의 수만 입력하시오.",
                    "Email": "이메일이 형식에 맞지 않습니다."
                }
            });
        });   
    </script>
    <h2>Create</h2>
    <% using (Html.BeginForm()) { %>
    <div>
        이름 : <%= Html.TextBox("Name") %>
    </div>
    <div>
        전화번호 : <%= Html.TextBox("Phone")%>
    </div>
    <div>
        단축다이얼 : <%= Html.TextBox("SpeedDial")%>
    </div>
    <div>
        이메일 : <%= Html.TextBox("Email")%>
    </div>
    <input type="submit" value="Create" />
    <% } %>
</asp:Content>

소스를 보시면(빨간색) jQuery 스크립트와 유효성 검사를 위한 스크립트를 추가하였습니다. 또한 유효성 검사를 담당하는 jQuery 스크립트 구문도 추가하였습니다.

자, $("form").validate() 를 통해 유효성 검사를 합니다. 보시는대로, rulesmessages를 통해 에러를 표시하게되죠. Email을 제외한 각 필드를 필수값으로 세팅을 했고( required: true), 이름은 5자 이내(maxlength :5), 단축다이얼은 1~99까지의 숫자를 받도록(range[1,99]), 이메일은 이메일 형식을 체크(email: true)하도록 하였습니다. 이밖의 옵션들은 여기서 확인하실 수 있습니다.
빈값으로 폼을 전송하려고하면 클라이언트단에서 이에 제재를 가하게 됩니다.


이메일(필수값 아님)을 제외한 나머지는 에러가 났습니다. 올바른 값을 하나하나 입력하면 바로바로 에러메시지가 사라지는 것을 확인할 수 있습니다.


이메일을 잘못입력하면 에러메시지가 뜨는 것도 확인할 수 있습니다.

여기까지 잘 따라오셨으면 보다 싶게 클라이언트단에서의 유효성 검사를 진행해보죠. (윗부분은 이제 잊어도 좋습니다. 딱히 잊으라는게 아닌 아래 소스에서는 필요가 없어서.. 이렇게 말씀드리는건데...음.. '아 이런방법도 있구나'만 기억하시면 됩니다.^^;;)

DRY 잊지말자

지난번 포스팅에서는 서버단에서 유효성 검사를 하였기때문에 유효성 에러 메시지를 보려면 서버단까지 다녀와야할 필요가 있었습니다. 이를 가만히둘 마이크로소프트가 아닙니다. 정말 심플한 방법으로 손쉽게 클라이언트단과 서버단 두군데 모두 유효성검사를 할 수 있도록 하는 단 세줄의 코드가 있습니다.

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MvcTest.Models.TelDir>" %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
 Create
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <script src="/Scripts/MicrosoftAjax.js" type="text/javascript"></script>
    <script src="/Scripts/MicrosoftMvcValidation.js" type="text/javascript"></script>
    <% Html.EnableClientValidation(); %>
    <h2>Create</h2>
    <% using (Html.BeginForm()) {%>       
        <fieldset>
            <legend>Fields</legend>
           
            <div class="editor-label">
                <%: Html.LabelFor(model => model.Name) %>
            </div>
            <div class="editor-field">
                <%: Html.TextBoxFor(model => model.Name) %>
                <%: Html.ValidationMessageFor(model => model.Name) %>
            </div>
           
            <div class="editor-label">
                <%: Html.LabelFor(model => model.Phone) %>
            </div>
            <div class="editor-field">
                <%: Html.TextBoxFor(model => model.Phone) %>
                <%: Html.ValidationMessageFor(model => model.Phone) %>
            </div>
           
            <div class="editor-label">
                <%: Html.LabelFor(model => model.SpeedDial) %>
            </div>
            <div class="editor-field">
                <%: Html.TextBoxFor(model => model.SpeedDial) %>
                <%: Html.ValidationMessageFor(model => model.SpeedDial) %>
            </div>
           
            <div class="editor-label">
                <%: Html.LabelFor(model => model.Email) %>
            </div>
            <div class="editor-field">
                <%: Html.TextBoxFor(model => model.Email) %>
                <%: Html.ValidationMessageFor(model => model.Email) %>
            </div>
           
            <p>
                <input type="submit" value="Create" />
            </p>
        </fieldset>
    <% } %>
</asp:Content>

위 세줄을 추가함으로 지난번 포스팅에서 DataAnnotation을 이용한 유효성 검사 로직을 클라이언트단에서도 사용할수 있게 되었습니다. 실행을 시킨 후, Create 버튼을 클릭하면 리로드없이 즉각적으로 에러메시지를 확인할 수 있습니다.


또한, 유효한 값을 입력하면 즉시 에러메시지가 사라집니다.
저희는 지금 클라이언트단에 유효성 검사 로직을 추가하지 않았습니다. 유효성 검사로직은 모델클래스에만 존재하고 있습니다. 하하하.(승리자의 웃음인거죠^^) 룰은 한 곳에다가 두고, 두군데(클라이언트와 서버)에서 모두 검사를 하도록 하였습니다. 이로써 DRY를 잊지 않은체 작업이 완료되었습니다.

마무리요

이렇게 손쉽게 클라이언트단에서도 검사가 가능한 방법이 있었습니다. ㅎㅎ 기분좋네요.
마이크로소프트는 현재 jQuery 프로젝트에 참여하여 계속 플러그인을 개발중에 있습니다. (이 얘기는 왜하는 걸까요? 음..) 이 부분에 대해서도 포스팅을 하도록 노력해보겠습니다.


참고자료 :
http://docs.jquery.com/Plugins/Validation
http://weblogs.asp.net/scottgu/archive/2010/01/15/asp-net-mvc-2-model-validation.aspx

Asynchronous Agents Library
– message 전달 함수. 1 ( 전송 )

작성자: 임준환( mumbi at daum dot net )

 

Message 메커니즘

 Asynchronous Agents Library( 이하, AAL ) 에서 message 메커니즘이란 agent 들 간의 데이터 교환이나 동기화 등 상호 작용을 위해 사용되는 기능입니다.

 Message 메커니즘은 크게 message 를 보내고( send ) 받는( receive ) 전달 함수( passing function )와 message 들을 관리하거나 message 에 특별한 기능을 부여하는 message block 으로 구성되어 있습니다.

 실질적으로 message block 이 다루는 message 란 빌트인 자료 형( built-in type )이나 클래스와 같은 사용자 정의 자료 형( user define type ) 의 데이터입니다.

 이 글에서는 먼저 message 를 주고 받는 전달 함수 중 전송 함수에 대해서 알아보겠습니다.

 

Message 전송

 Message 전달 함수 중 보내는 기능을 하는 함수에는 동기 전송 함수 send() 와 비 동기 전송 함수 asend(), 이렇게 두 가지가 있습니다.

 동기와 비 동기라는 용어가 혼란스러울 수 있기 때문에 잠깐 언급하고 넘어가겠습니다.

 여기서 쓰이는 동기( synchronous )라는 용어는 병렬 처리에서 쓰이는 동기화( synchronization )라는 용어와는 약간은 다른 개념입니다.

 동기화는 다른 두 시간을 하나로 일치시킨다는 뜻으로 행위를 말합니다. 반면 동기는 이미 동기화되었다는 뜻으로 상태를 뜻합니다. 마찬가지로 비 동기는 동기화되지 않았다는 뜻입니다.

 즉, 보통 프로그래밍에서 동기 함수란 그 함수가 호출되고, 그 함수가 반환될 때까지 해당 컨텍스트가 진행되지 않고 기다리다가 반환되고 나서야 컨텍스트가 진행되는 함수를 말합니다. 이것은 사실 컨텍스트가 기다리는 것이 아니라, 해당 컨텍스트가 함수의 내용을 직접 처리하기 때문에 함수를 호출한 입장에서 보면 기다리는 것처럼 보이는 것입니다.

 마찬가지로 비 동기 함수는 함수를 호출한 컨텍스트가 직접 함수의 내용을 처리하지 않고 새로운 작업 스레드를 생성하고 생성된 스레드의 컨텍스트가 진행되기 때문에 함수를 호출한 컨텍스트는 함수를 호출하자마자 함수의 반환을 받고, 계속해서 진행되는 것입니다. 함수를 호출한 컨텍스트는 이러한 비 동기 함수가 언제 실제로 종료될지 모르기 때문에 함수의 반환이 아닌 다른 기법이 필요합니다. 보통 폴링( polling )이나 메시지 또는 콜백 함수와 같은 기법을 사용하여 함수의 종료를 알 수 있습니다.

 그럼 이제 본격적으로 두 message 전달 함수에 대해서 알아 보겠습니다.

 

동기 전송 함수 send()

 앞에서 설명한 것처럼 send() 는 동기 함수이기 때문에 message 가 전송에 대한 결과가 확실해 졌을 때 반환됩니다. 즉, 전송된 결과가 확실할 때까지 기다린다는 뜻입니다.

  send() 의 선언은 다음과 같습니다.

template <
   class _Type
>
bool send(
   ITarget<_Type> * _Trg,
   const _Type& _Data
);

template <
   class _Type
>
bool send(
   ITarget<_Type> &_Trg,
   const _Type &_Data
);

[ 코드1. send() 의 선언 ]

 템플릿 매개변수인 _Type 은 전송할 message 의 자료 형입니다.

 함수 매개변수 중 _Trg 는 message block 의 인터페이스 중 하나인 ITarget 을 상속받은 message block 객체이며, 전송될 message 를 받게 됩니다. 나중에 message block 에 대해서 자세히 언급할 예정입니다

 또 다른 함수 매개변수인 _Data 가 바로 전송할 message 입니다.

 send() 가 message 전송에 성공했으면 true 를, 그렇지 않으면 false 를 반환합니다.

예제

#include <agents.h>

using namespace Concurrency;

int main()
{
	// message block
	unbounded_buffer< int > message_block;

	send( message_block, 1 );	
}

[ 코드2. send() 예제 ]

 아직 message block 에 대해서 설명하지 않았지만 예제를 위해 message block 중 하나인 unbounded_buffer 를 사용하였습니다.

 message_block 에 1 을 message 로 전송하는 코드입니다. 아래의 캡처 그림을 통해 message 가 message block 에 전송된 것을 확인할 수 있을 것입니다.

 아직 수신 함수를 설명하지 않았기 때문에 코드의 실행 결과가 아닌 코드의 디버깅 화면을 캡처한 그림으로 대신하겠습니다.

[ 그림1. send() 예제 디버깅 화면 ]


 

비 동기 함수 asend()

 asend() 는 비 동기 함수입니다. 즉, 전송이 완료되기 전에 반환됩니다.

 send() 는 전송 결과를 반환하는 반면, asend() 는 전송 결과가 아닌 message 를 받는 message block 이 전송을 수락했는지 아닌지를 반환합니다.

 아래는 asend() 의 선언입니다.

template <
   class _Type
>
bool asend(
   ITarget<_Type> * _Trg,
   const _Type& _Data
);

template <
   class _Type
>
bool asend(
   ITarget<_Type> &_Trg,
   const _Type &_Data
);

[ 코드3. asend() 의 선언 ]

 템플릿 매개변수와 함수 매개변수는 모두 send() 와 같습니다.

예제

#include <agents.h>

using namespace Concurrency;

int main()
{
	// message block
	unbounded_buffer< int > message_block;

	asend( message_block, 1 );

	Concurrency::wait( 10 );
}

[ 코드4. asend() 예제 ]

 asend() 가 반환되었을 때에는 아직 message block 에 message 가 전송되지 않았습니다. 이것으로 asend() 가 비 동기 함수임을 확인할 수 있습니다.

 약간의 시간( 10 milli second ) 이 지난 후에는 message block 에 message 가 전송된 것을 확인할 수 있습니다.

[ 그림2. asend() 예제 디버깅 화면 - 호출 직 후 ]


[ 그림3. asend() 예제 디버깅 화면 - 약간의 시간이 지난 후 ]


 

Message 필터

 Message 전송 함수인 send() 의 반환 값이 전송 결과라고 하였고, 실패할 경우 false 를 반환한다고 하였습니다. 사실, 실패할 경우란 message 를 받는 message block 이 전송을 거부할 경우, 즉 필터링되었을 경우입니다.

 결론적으로 send() 와 asend() 의 반환 값은 모두 message block 의 수락 또는 거절 여부입니다.

 여기서 집고 넘어가야 할 부분이 언제 전송이 거부되는 것인가 하는 것입니다.

 message block 은 두 가지 경우에 message 전송을 거부합니다.

 첫째는 message block 이 파괴되어 소멸자가 처리되고 있을 때입니다. 당연한 상황입니다.

 둘째는 message block 의 필터에 의해 message 가 거부당했을 때입니다. 모든 message block 의 생성자 중에는 filter_method 형의 매개변수를 갖는 생성자가 있습니다. filter_method 형은 사실 std::tr1::function<bool(_Type const&)> 입니다. message block 을 생성하는 클라이언트는 임의의 message 필터를 적용할 수 있습니다. 이 필터 함수가 false 를 반환할 경우, message 전송은 거부됩니다.

예제

#include <iostream>
#include <agents.h>

using namespace std;
using namespace Concurrency;

int main()
{
	// 필터( 짝수만 수락 )가 적용된 message block
	unbounded_buffer< int > buffer( []( int message ) -> bool
	{
		return 0 == ( message % 2 );

	} );

	wcout << L"send 1: " << ( send( buffer, 1 ) ? L"accepted" : L"declined" ) << endl;	// 거부
	wcout << L"send 2: " << ( send( buffer, 2 ) ? L"accepted" : L"declined" ) << endl;	// 수락

	wcout << L"asend 3: " << ( asend( buffer, 3 ) ? L"accepted" : L"declined" ) << endl;	// 거부
	wcout << L"asend 4: " << ( asend( buffer, 4 ) ? L"accepted" : L"declined" ) << endl;	// 수락
}

[ 코드5. 전송 거부 예제 ]

 이 예제에는 message block 의 필터로 Visual studio 2010 에서 지원하는 C++0x 의 람다를 사용하였습니다. 람다는 이 글의 논제에서 벗어나기 때문에 설명하지 않도록 하겠습니다. Visual studio 팀 블로그에서 람다에 대한 정보를 얻을 수 있습니다.

 간단히 람다에 대해서 설명하고 넘어가자면 익명의 함수 객체라고 보셔도 될 것입니다. 

 예제에 사용된 message block 의 필터는 짝수만 수락하는 필터입니다. 그래서 실행 결과로 send() 와 asend() 모두 홀수는 거부되었고, 짝수는 수락되는 것을 볼 수 있습니다.

[ 그림4. 전송 거부 예제 실행 결과 ]


 

마치는 글

 이 글에서는 message 전달 함수 중 전송 함수들에 대해서 알아보았습니다.

 이 함수들 중 어떤 것을 사용하는 것이 적절한지를 판단하기 위해서는 반드시 동기와 비 동기에 대한 개념의 이해가 필요합니다.

 상황에 따라 적절한 함수를 사용하시면 원하는 결과를 얻을 수 있을 것입니다.

 전송 함수들에 대해서 알아보았지만 아직 수신 함수들에 대해 알아보지 않았습니다. 다음 글에서는 message block 으로부터 message 를 수신하는 수신 함수들에 대해 알아보겠습니다.

6월 1일 REMIX10 행사를 기점으로 Visual Studio 2010 한글판이 대중에 공개가 되었습니다. Visual Studio 2010 의 영문 버전은 그 이전에 출시가 되었지만 한글판이 출시된 이후에 더 많은 관심을 받게 되었습니다.

Visual Studio 2010 으로 개발 환경을 업그레이드를 진행하는 곳이 특히 해외에서 많습니다. 제가 그걸 어떻게 다 아냐구요? 항상 트위터 검색을 통해 해외에서 Visual Studio 2010 를 어떻게 사용하고 있는지 매일 매일 관심 있게 보고 있답니다. ^^

어쨌든 Visual Studio 2008 을 쓰고 있지만, Microsoft MVP 이거나 회사에서 MSDN Subscription 라이선스가 있다면 Visual Studio 2008, Visual Studio 2010 개발 도구가 혼합해서 사용될 경우가 있습니다. 이런 경우 두 개발 도구에서 쌍방 개발 가능하게 구성을 할 수 있습니다.

이 방법은 제니퍼소프트의 정성태 과장님의 블로그에서 예전에 소개했던 VS2005, VS2008 혼합해서 사용하는 방법과 동일합니다.

1. 간단한 예제로 Console Application 을 Visual Studio 2008 에서 생성했습니다.

   

2. 기존의 솔루션 파일의 복사본을 하나 만듭니다.

   

3. 솔루션 파일을 노트패드로 ConsoleApplication - VS2010.sln 파일을 열어 다음의 항목을 수정합니다.

   

4. 프로젝트 파일을 열어 ToolsVersion 의 '3.5' 를 '4.0' 으로 수정합니다. 'ConsoleApplication1.csproj'

  

만약 다수의 프로젝트일 경우, 위의 3번에서 수정한 솔루션 파일을 열면 프로젝트를 변환하는 마법사로 진행하면 쉽게 변경이 됩니다.

   

5. 모두 완료 되었습니다. Visual Studio 2010 와 Visual Studio 2008 에서 각각의 솔루션 파일로 동시에 작업을 할 수 있습니다.

   

위의 방법을 이용하여 Visual Studio 2008 과 Visual Studio 2010 에도 모두 개발이 가능합니다. 하지만 만약 테스트 프로젝트가 포함이 되어 있다면 두 개발 도구에서 사용할 수 없습니다. 왜냐하면 .NET Framework 4.0 에서는 테스트와 관련된 Microsoft.VisualStudio.Quality 프레임워크가 개선되고, Microsoft.VisualStudio.TestTools 프레임워크가 추가되면서 이전 Visual Studio 2008 과 프로젝트가 호환이 되지 않습니다.    

하지만 불가능할 것 같은 테스트 프로젝트도 Visual Studio 2008과 Visual Studio 2010에서 동시에 사용할 수 있는 방법이 있습니다. 이것은 다음에 알아보도록 하겠습니다.

Welcome to Parallel C#(9) - 백지장은 맞들지 말엉.

C# Parallel Programming 2010. 6. 22. 09:00 Posted by 알 수 없는 사용자
- 이건 또 무슨 신개념 속담 드립인거.

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


- 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

Welcome to Parallel C#(8) - 취소 쉽게 하기.

C# Parallel Programming 2010. 6. 17. 09:00 Posted by 알 수 없는 사용자

- 취소는 어렵지.

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


- 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

Visual Studio 2010 이 출시된 이후 짧은 시간 동안 많은 변화가 있었습니다. 그리고 그 변화의 움직임을 직접 느끼고 있고요. 많은 기업과 고객, 그리고 여러분들께서 보여주신 관심은 기대 이상이었습니다. 

이에 질세라 저희 Visual Studio 2010 공식 팀에서 여러분들을 위해 많은 문서를 무료로 만들어 배포하고 있습니다.

   

   

아래의 링크에서 방대한 양의 문서를 PDF 로 무료로 다운로드하실 수 있습니다.
http://msdn.microsoft.com/ko-kr/vstudio/default.aspx

   

Visual Studio 2010

Visual Studio 2010 필요한 10가지 이유-라인업 기능소개 브로셔
Visual Studio 2010 UPGRADE NOW 프로모션 브로셔
Visual Studio 2010 라이센스 가이드
Visual Studio 2010 활용한 ALM 백서 (.NETXPERT 재직 중 엄준일 저)

Team Foundation Server 2010

Team Foundation Server 2010 설치 가이드 (다중서버) (.NETXPERT 재직 중 엄준일 저)
Team Foundation Server 2010 활용가이드 (FQDN) (.NETXPERT 재직 중 엄준일 저)
Team Foundation Server 2010 설치 가이드 (단일서버) (.NETXPERT 재직 중 엄준일 저)
Team Foundation Server 2010 설치 가이드 (Lab 환경구성) (.NETXPERT 재직 중 엄준일 저)

Language

First Look C#4.0 백서 (강보람 저)
Visual Studio 2010 C++0x 백서 (최흥배 저)

Visual Studio 2010 단축키

Visual Studio 2010 Visual Basic 단축키 포스터 (방수철님 역)
Visual Studio 2010 Visual F# 단축키 포스터 (방수철님 역)
Visual Studio 2010 Visual C++ 단축키 포스터 (방수철님 역)
Visual Studio 2010 Visual C# 단축키 포스터 (방수철님 역)

지난 6월 1일, 서울 삼성동 코엑스 인터컨티넨탈 호텔에서 호화스럽게 열린 REMIX10 행사가 있었습니다. 이날 행사에서는 천 여명 훌쩍 넘는 분들이 참석하셨습니다.    

많은 분들이 평일에 개최된 행사라 사정이 여의치 않은 분들께서는 참석하지 못하신 것으로 알고 있는데, Microsoft Korea 에서 이날 모든 세션을 녹화하여 드디어 REMIX10 행사의 모든 세션이 공개가 되었습니다.    

비록 이 날 참석하지 못하신 분들도, 아래의 링크를 통해 녹화된 비디오를 시청하실 수 있습니다. http://www.visitmix.co.kr/remix10/agenda.html

특히, 이 날 키노트 세션은 모든 세션의 중요 정보를 한 눈에 볼 수 있는 중요한 시간이기도 합니다. 모든 세션을 시청하기 시간이 부족하신 분들이라면, 키노트 세션은 꼭 한번 보시라고 권장합니다.
http://www.techdays.co.kr/2010spring/remix10/keynote.html    

[MFC/윈도우 7 멀티터치] #3 : 제스처(gesture)를 이용한 구현(下)

MFC 2010. 6. 16. 09:00 Posted by 알 수 없는 사용자

Intro

안녕하세요~ MFC 카테고리의 꽃집총각 입니다.

이번 포스팅에서는 앞 글에서 이야기 중이었던 예제 프로그램의 나머지 부분을 마저 작성하고, MFC 기반의 프로젝트에서 제스처를 이용해 멀티터치 프로그래밍을 제어하는 방법에 대한 보충 설명을 정리해 보도록 하겠습니다. 앞에 적은 글과 이어지는 내용이니까, 앞부분을 보지 못하신 분들은 ( http://vsts2010.net/292 ) 이 글을 먼저 읽어주세요 ^^ 지난번 글에서는 멀티터치 동작을 확인하기 위한 Drawing 처리까지 함께 알아봤습니다. 앞부분에서 진행됐던 내용을 소제목만 간추려서 알아보면,

* 제스처를 인식하는 첫 번째 예제 프로그램 *

  1. MFC Application 프로젝트를 만들자
  2. 하드웨어 상태를 알아봅시다
  3. View 영역에 사각형을 그리자!

…와 같은 내용이었습니다. 두 번째 스텝에서 알아봤던 하드웨어의 멀티터치 인식 가능여부 확인방법을 제외하고는 멀티터치와 직접적으로 연관된 내용은 많이 없었군요. 이번 시간부터는 본격적으로 터치 입력을 제어하는 코드를 함께 알아보겠습니다.

 

Task 4 : 터치 인식 코드를 넣을 차례!

자, 이제 드디어 제스처를 제어하는 코드를 알아볼 시간입니다. 사용자 경험(UX;User Experience) 분야에서 가장 뜨거운 이슈인 멀티터치! 뭔가 색다르고 놀라운 구현방식이 있을 거 같지만 실망(?)스럽게도 구현방법은 기존의 MFC 프로그래밍과 별다른 바가 없습니다. 그저 적당한 함수를 오버라이딩 해서 필요한 코드를 넣어주면 그만이거든요. 바로 이 점이 새롭게 변화된 MFC 추가기능들이 마음에 드는 이유 중에 하나입니다. 바로 ‘친숙한 인터페이스’ 말입니다 :)

MFC의 윈도우 랩핑 클래스인 CWnd에는 이번에 새롭게 추가된 제스처 관련 함수들이 있습니다. 이 중에서 아래의 다섯 가지 제스처 핸들링 함수를 오버라이딩 합니다. 각각의 함수들은 이름에 명시되어 있는 제스처가 입력됐을 때 호출됩니다. 아래의 코드를 ChildView.h 파일의 class CChildView 선언에 넣어줍니다.

// Overrides
protected:
    // Gesture handlers
    virtual BOOL OnGestureZoom(CPoint ptCenter, long lDelta);
    virtual BOOL OnGesturePan(CPoint ptFrom, CPoint ptTo);
    virtual BOOL OnGestureRotate(CPoint ptCenter, double dblAngle);
    virtual BOOL OnGesturePressAndTap(CPoint ptFirstFinger, long lDelta);
    virtual BOOL OnGestureTwoFingerTap(CPoint ptCenter);

기본적으로 터치가 인식되면 애플리케이션으로 WM_GESTURE 메세지가 날아옵니다. Win32 API만을 이용해 제스처를 제어하려면 WndProc 프로시저 함수에서 WM_GESTURE를 잡아서 처리해야 하지만, MFC 애플리케이션의 경우 자체적으로 각각의 제스처에 대한 전용 핸들링 함수들이 나뉘어져 있기 때문에, 제어가 필요한 제스처에 해당하는 함수를 용도에 맞게 따로따로 오버라이딩 해서 사용하면 됩니다.

Note : 그런데 CWnd의 선언을 살펴보면 (afxwin.h 파일에 있습니다.) 위에 나열된 함수 말고도 OnGesture 함수가 정의되어 있는 것을 확인하실 수 있습니다. 함수의 원형은 아래와 같습니다.

afx_msg LRESULT OnGesture(WPARAM wParam, LPARAM lParam);

오호라~ 이것은 WM_GESTURE 메세지가 넘어올 때마다 해당 메세지를 직접 제어할 수 있는 함수인가 보군요~ 함수의 인자로 wParam, lParam이 모두 날아오니 win32 프로젝트에서 하는 것처럼 코딩하고 싶을 땐 이걸 상속받아서 작업하면 되겠구나 ~ … 라고 생각하고, 실제로 테스트 코드도 만들어 봤었지만 안되더군요 @.@… 똑똑한 여러분들은 이유가 무엇인지 바로 찾으셨을 거라고 생각합니다만… 저 함수는 가상 함수가 아닙니다 ^^;… 그저 이름만 보고 상속받아 써야지 했는데, 나중에 보니 재정의 할 수 없는 일반 멤버함수더라고요 ㅎㅎ 아마도 CWnd가 자체적으로 처리하는 코드를 구현한 부분이 아닌가 생각됩니다. 일반적으로는 각각의 제스처마다 독립적으로 호출되는 위의 다섯 가지 함수를 이용하면 되고요, 경우에 따라 부득이하게 WM_GESTURE 메세지를 직접 제어하고 싶을 때엔… 이전에 그랬던 것처럼 WndProc을 직접 제어하도록 하고 그곳에서 WM_GESTURE를 받은 경우에 대한 switch-case 문을 넣어주면 되겠죠 ^^

이제 위에 소개된 다섯 개의 함수들에서 view 영역에 그려지고 있는 사각형을 제어하는 코드들을 넣어줍니다. 아래에 함수들의 본문 코드가 있습니다.

BOOL CChildView::OnGesturePan(CPoint ptFrom, CPoint ptTo)
{
    int dx = ptTo.x - ptFrom.x;
    int dy = ptTo.y - ptFrom.y;
    
    if (dx != 0 || dy != 0)
    {
        m_drawingObject.Move(dx, dy);
        RedrawWindow();
    }
    return TRUE;
}
 
BOOL CChildView::OnGestureZoom(CPoint ptCenter, long lDelta)
{
    if ((m_pCurrentGestureInfo->dwFlags & GF_BEGIN) == GF_BEGIN)
    {
        m_dblZoomRatioStart = m_dblZoomRatioTotal = lDelta;
    }
    else if (lDelta != 0)
    {
        m_dblZoomRatioTotal += lDelta;
        double zoomFactor = (double)m_dblZoomRatioTotal / m_dblZoomRatioStart;
        
        m_drawingObject.Zoom(zoomFactor, ptCenter.x, ptCenter.y);
        
        m_dblZoomRatioStart = m_dblZoomRatioTotal;
        RedrawWindow();
    }
    return TRUE;
}
 
BOOL CChildView::OnGestureRotate(CPoint ptCenter, double dblAngle)
{
    if ((m_pCurrentGestureInfo->dwFlags & GF_BEGIN) == GF_BEGIN)
    {
        // Make the first center, the rotating one
        m_ptCenter = ptCenter;
    }
    else if (dblAngle != 0.)
    {
        m_drawingObject.Rotate(dblAngle * PI / 100.0, m_ptCenter.x, m_ptCenter.y);
        RedrawWindow();
    }
    
    return TRUE;
}
 
BOOL CChildView::OnGesturePressAndTap(CPoint ptFirstFinger, long lDelta)
{
    if ((m_pCurrentGestureInfo->dwFlags & GF_BEGIN) != 0)
    {
        m_drawingObject.ShiftColor();
        RedrawWindow();
    }
    
    return TRUE;
}
 
BOOL CChildView::OnGestureTwoFingerTap(CPoint ptCenter)
{
    m_drawingObject.TogleDrawDiagonals();
    RedrawWindow();
    
    return TRUE;
}
 

함수의 이름과 전달되는 인자들 모두 직관적입니다. 함수 본문의 예제 코드들도 그리 어렵지 않군요. 위의 코드들을 복사해 적어준 뒤 빌드하고 프로그램을 실행해 봅니다. 애플리케이션이 뜨면 사각형을 손으로 움직여 보세요. 터치 입력에 따라 사각형은 이동하고, 늘어나고, 회전할겁니다 :)

네…? 근데 뭔가가 안 된다고요?

 

Task 5 : 다 되는 거 같은데 회전만 안되네요 ㅡㅜ…

여기까지 진행하고 테스트를 해보면 다른 제스처는 다 인식을 하는데, 회전(rotate)만 제대로 안 되는 현상을 겪으실 겁니다. 참고로 이번 예제에서 확인하실 수 있는 다섯 가지 제스처에 대한 동작을 간략히 설명 드리면 아래와 같습니다.

panning 손가락을 스크린에 대고 이동한다.
zoom 두 손가락을 스크린에 대고 벌리거나 모은다.
rotate 두 손가락을 스크린에 대고 회전시킨다.
press and tab 한 손가락을 스크린에 댄 상태에서 다른 손가락으로 스크린을 빠르게 터치한다.
two finger tab 두 손가락을 동시에 스크린에 붙였다 뗀다.

그런데 다른 건 다 잘되는데 아마 rotate 제스처만 동작하지 않을 거예요. 그리고 다른 제스처들은 다 인식할 테지만… 사각형 내부에서 일어난 제스처인지를 판단하는 처리가 없었기 때문에, 꼭 사각형 내부가 아니더라도 client 영역 내에서 발생한 유효한 제스처라면 모두 다 인식하는 걸 확인할 수 있으실 겁니다.

그 이유는 코드가 잘못된 것이 아니라, 윈도우 자체에서 다른 제스처들은 모두 기본적으로 활성화 되어있는데, rotate만은 기본적으로 비활성화 되어있기 때문입니다. 활성화 하려면 다 해두든가 아님 말든가 할 것이지 왜 rotate만 천대(?)하는지 자세한 내막은 모르겠습니다만… 그것이 현실이군요. rotate를 활성화하기 위해, 제스처의 설정(config)를 제어하는 방법을 알아보도록 합시다.

MFC에서는 제스처의 설정을 쉽게 컨트롤 할 수 있도록 CGestureConfig라는 클래스를 제공합니다. CChildView의 선언( in ChildView.h )에 CGestureConfig 타입의 멤버변수를 하나 추가해줍니다.

class CChildView 
{
    // ...
 
// Fields
protected:
    // Holds gesture configuration
    CGestureConfig m_gestureConfig;
    
    // ...
}

그리고 새롭게 부활한 반가운 인터페이스, 마법사 중의 마법사 MFC Class Wizard를 띄워서 OnCreate 함수를 오버라이딩 합니다. (단축키 Ctrl + Shift + X 입니다 ^^)

그리고 아래의 코드를 넣어주세요.

 
int CChildView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    if (CWnd::OnCreate(lpCreateStruct) == -1)
        return -1;
 
    // TODO: Add your specialized creation code here
    GetGestureConfig(&m_gestureConfig);
    
    // Only rotation is not enabled by default
    m_gestureConfig.EnableRotate();
    
    SetGestureConfig(&m_gestureConfig);
 
    return 0;
}
 
 
 
그리고 나서 다시 한번 빌드하고 실행해 봅니다. 이제 rotate 제스처도 제대로 인식하는 첫 번째 예제 프로그램이 완성 되었습니다 ^^*
 
자 드디어 첫 번째 예제의 완성입니다~ !! 짝짝짝 ~
 

Task 6 : panning이 왜 이렇게 뻑뻑하지? 아이폰은 안 그런뎅..

 
task5 까지가 channel9에서 소개한 MFC gesture 기능의 첫 번째 예제입니다. 필요한 부분과 불필요한 부분을 적절하게 구분해 잘 설명해 두어서, 한번 설명을 따라 작성해 보시는 것만으로 제스처 프로그래밍에 대한 대략적인 흐름을 손쉽게 파악하셨을 거라고 생각합니다.
 
이대로 끝내기엔 좀 아쉬운 감이 있어서… 예제 코드에 대한 추가 설명을 드리고자 합니다. 사실 글 중간중간에도 제가 추가로 적은 글이 제법 있었지만… 마땅히 말씀드릴 타이밍을 못 찾았던 요것 한 가지만 더 말씀 드리도록 할게요 ㅎㅎ
 
일단 예제를 조작해 보면 제일 먼저 거슬리는 부분이 바로 panning 제스처에 대한 조작감(?) – 게임개발만 오래 하다 보니 이런 표현이…^^; – 인데요. 아이폰에서 느껴지는 부드러운 반응성에 비해 사각형 움직임이 무척이나 둔하고 불만족스럽다는 걸 느끼실 겁니다. 사각형이 한 번 움직이고 나면 괜찮은데, panning 제스처를 인식해 움직여지기 시작할 때 까지가 뭔가 걸리는 느낌입니다. x, y 한 방향으로만 움직이고, 다른 방향으로는 안 움직이는 경우도 아마 겪으셨을 거예요. 왜 이럴까요? 기기의 한계? 아님 OS 자체의 한계?
 
이런 현상은 하드웨어의 문제나 OS 기능 자체의 문제가 아닙니다. 이것도 역시 panning 제스처의 옵션에서 GC_PAN_WITH_GUTTER 플래그가 기본적으로 켜져 있기 때문에 나타나는 현상입니다. 저도 처음에 제스처(gesture)를 이용한 멀티터치 예제들을 실행해 보다가 panning의 무딘 반응성이 너무 눈에 거슬려서, 부드러운 움직임을 보려면 결국 WM_GESTURE에서는 한계가 있고, WM_TOUCH를 써야 하는가 보다 했었는데, 간단히 플래그만 조절해주면 좀 더 부드러운 움직임을 느낄 수가 있게 됩니다.

GC_PAN_WITH_GUTTER에서 gutter는 홈통이나 배수로 등의 뜻으로, panning 제스처가 발생했을 때 가장 메인이 되는 방향으로의 움직임 이외의 방향성에 대한 반응은 특정 임계값을 넘기지 않는 이상 무시하게 하는 효과를 줍니다. 이 때문에 손가락에서의 미세한 움직임들이 무시되고, 반응성이 안좋다는 느낌을 받게 되죠. 이를 해결하기 위해서는 rotate 제스처의 활성화를 위해 오버라이딩 했었던 OnCreate 함수에서 panning에 대한 아래의 설정도 함께 처리해 주면 됩니다.

 
config.EnablePan( TRUE, 
    GC_PAN_WITH_SINGLE_FINGER_VERTICALLY | 
    GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY |
    GC_PAN_WITH_INERTIA );
 
pan 제스처를 활성화 하되, GC_PAN_WITH_GUTTER 플래그만을 제외한 나머지 플래그들을 활성화 해주는 코드입니다. 위 코드를 넣고 나서 빌드하고 다시 실행해보면 이전보다 훨씬 더 부드럽게 사각형이 이동되는 것을 느낄 수 있으실 겁니다.

 
 

Outro

이것으로 MFC를 이용해서 제스처를 이용한 멀티터치 프로그래밍 방법의 첫 번째 예제를 소개와 설명을 모두 마쳤습니다. 제스처의 설정을 컨트롤 하는 방법, 각각의 제스처에 대한 핸들링 코드를 넣어주는 방법 등을 알아보았는데, 대부분 예전 MFC 프로그래밍의 마우스/키보드 이벤트 처리 등과 비슷한 방식이었기 때문에 그리 어렵지는 않은 난이도였습니다.

다음 포스팅 에서는 이제 가장 자유롭고 확장성 있는 (… 하지만 대신 좀 더 까다로운 ) WM_TOUCH를 이용한 멀티터치 프로그래밍 방법을 알아 보도록 하겠습니다. 혹시 그 전에 질문 사항들이나, 제스처 프로그래밍 방법들에 대한 추가 학습사항이 있다면 다시 한 번 정리하는 기회를 갖도록 하겠습니다.

그럼 다음 포스팅에서 다시 인사 드리도록 하겠습니다. 그 때까지 더운 날씨에 모두들 건강하시고 공부 열심히 하세요 ~ ^^*

감사합니다 ~ 꾸벅 ~

Reference

[JumpToDX11-13] Tessellation 등장.

DirectX 11 2010. 6. 15. 09:00 Posted by 알 수 없는 사용자


요즘 vs2010 에 아티클이 많아져서 너무 좋습니다..^^


< Tessellation 개념 잡기 >

지금부터 언급할 내용은 Tessellation 입니다.
Tessellation을 간단히 정의하자면, 적은 수의 폴리곤이 그래픽 파이프라인을 통과했을 때,
많은 수의 폴리곤들을 생성해서 최종 화면에서는 훨씬 정밀한 결과를 나타내는 기술
이라고
할 수 있습니다.
간단히 이미지를 보시면 쉽게 개념화 할 수 있을 것입니다.






감동적이신가요?
저는 꽤 큰 감동을 받기는 했는데, 어마어마한 연산량이 걱정이 되었습니다.
물론 여러 분들도 저와 같은 생각일 것이라고 생각합니다.



< Tessellation 의 등장 배경 >

오늘 날의 컴퓨터 그래픽스의 발전은 정말이지 급격하게 변화했습니다.



유명한 파이날 판타지7 과 최신작을 비교해 보았습니다.
그래픽적인 큰 변화가 느껴지시나요?
아마도 느껴지실 것입니다.( 안느껴지시면 곤란합니다..^^ )

저 변화의 중심에 서 있는 기법 혹은 기술은 어떤 것일까요?
다양한 의견이 있을 수 있지만, 개인적인 견해를 전제로 제가 언급하면 텍스쳐링이라고 생각합니다.
오늘 날의 하나의 폴리곤에 적용되는 텍스쳐의 갯수는 하나가 아닙니다.
노말맵이 거의 표준적으로 사용되고 있는 현 세대에서는
각종 라이팅 처리를 위해서 많은 갯수의 텍스쳐가 사용되고 있습니다.
그래서 우리는 현실감 있는 게임을 즐길 수 있습니다.
이러한 발전의 방향은 폴리곤 갯수를 증가시키는 것보다,
텍스쳐링을 활용하는 것이 성능적인 측면에서 더욱 효과적이기 때문입니다.

그러던 과정에서 이제는 GPU의 성능이 급격히 발전하기 시작했습니다.
많은 사람들이 GPU의 활용에 대해서 고민하기 시작했고,
DirectX9 부터 이런 GPU을 활용한 Tessellation 위한 기법들이 공개적으로 소개되기 시작했습니다.
특히나 ATI 쪽에서는 DirectX9 을 위한 Tessllation SDK 를 제공했었습니다.
여담이지만, 엔비디아쪽에서는 자사의 GPGPU 인 CUDA 를 DirectX9 에서 지원했었습니다.
두 회사의 발전 방향이 이때부터 사실 조금씩 차이가 나기 시작했었다고 볼 수 있습니다.



위의 그림은 ATI 사에서 Tessellation의 필요성을 표현하고 있는 그림입니다.
텍스쳐링을 아무리 많이해도, 폴리곤 갯수가 적으면 더 큰 현실감을 느끼는데는 제한이 있다는 정도로 정리할 수 있을 것입니다.
( 그림(c) 에서 몬스터의 부자연스러운 손가락이 보이시죠? )

그래서 조금 더 큰 현실감을 위해서 폴리곤을 증가시키는 방법을 고안하게 되었고,
급기야 이것이 현 DirectX11 의 정식 그래픽 파이프라인 스테이지로 추가되었습니다.
즉, 공부할 것이 훨씬 더 많아졌습니다...T.T


< 왜 Tessellation 인가? >

조금 과장된 표현을 해서, 게임에서 폴리곤을 많이 사용하는 것은 범죄(?) 행위에 해당합니다.
그래픽 카드가 놀라운 속도로 발전을 하고 있지만,
아직도 게임 개발자들은 비디오 메모리의 부족을 호소하고 있습니다.
당연한 얘기지만, 이는 폴리곤 갯수와 퀄리티의 증가에 의한 것입니다.



위의 그림처럼 그래픽 카드는 약간 독특한 성능을 가지고 있습니다.

첫번째로 대역폭입니다.
CPU쪽 대역폭보다 훨씬 크기 때문에, 대량의 데이터를 전송할 수 있습니다.

두번째는 비디오 메모리가 시스템 메모리 보다 훨씬 작다는 것입니다.

세번째는 수치 연산과 병렬연산에 강한 GPU 라는 것입니다.
실제로 Tessellation 파이프라인 스테이지는 병렬적으로 처리됩니다.
( 다음 시간에 이에 대한 언급이 있을 것입니다. )

결과적으로 Tessellation 의 이점은
폴리곤 갯수를 줄임으로써 비디오 메모리 사용량을 감소시킵니다.
이는 결국 적은 데이터 전송으로 인해 대역폭을 절약할 수 있습니다.
하지만, Tessellation 은 GPU 의 성능에 좌우된다고 할 수 있습니다.
연산량이 실제로 많기 때문에, 정말이지 빠른 성능이어야 한다는 것입니다.
다행스러운 것은 GPU 의 성능이 비디오 메모리의 확장보다는 더 빨라지고 있다는 것입니다.

사실 Tessellation 에 대한 가장 큰 의구심은 '과연 빠를까?' 입니다.
이것에 대한 정답은 아직은 없습니다.
적절한 곳에서 사용한다면 유용할 수도 있을 것이고, 그렇지 않을 수도 있을 것입니다.
다만, 현재 DirectX 의 새로운 패러다임으로 Tessellation 이 선택되어졌으며,
좋은 성능을 위해서 꾸준히 노력할 것이라는 것입니다.^^

Hello Windows Azure / Windows Azure Tools for Visual Studio 1.2 출시

Cloud 2010. 6. 14. 19:00 Posted by 알 수 없는 사용자

안녕하세요. Visual C# MVP 남정현입니다. 본 강좌를 올리기 전에 일종의 공지 사항 겸 소개를 위하여 이 글을 먼저 올리게 되었습니다. 얼마전에 Windows Azure Tools for Visual Studio 1.2 (2010년 6월 업데이트)가 새롭게 출시되었습니다. 메이저 버전도 아니고 마이너 버전인데 이와 같이 별도의 글을 써서 올리는 이유는, 다름이 아니라 Windows Azure Tools for Visual Studio의 기능에 많은 변화가 있었기 때문입니다.

Windows Azure SDK 1.2에서는 최근 발표된 .NET Framework 4.0 런타임을 내장한 Windows Azure OS 2010년 4월 버전에 대한 요구 사항을 모두 반영하고 있습니다. .NET Framework 4.0 기반의 응용프로그램을 작성 중이신 분들도 이제는 Windows Azure 환경 위에서 직접 응용프로그램을 호스팅할 수 있습니다. 그리고, Visual Studio 2010에서 새로 추가된 향상된 디버깅 기법인 IntelliTrace를 직접 지원하기 때문에 이전보다 더욱 쉬운 디버깅이 가능합니다.

다음은 Windows Azure Tools for Visual Studio 1.2의 변경 사항들입니다.

  • Visual Studio 2010 RTM 지원: Visual Studio 2010 RTM 버전을 지원합니다.
  • .NET Framework 4.0 지원: 이제는 .NET Framework 3.5와 4.0을 빌드 타겟으로 동시에 지정할 수 있습니다. 단, 4.0을 빌드 타겟으로 지정하게 되는 경우, Windows Azure OS의 이미지 중 2010년 초반의 일부 버전은 사용할 수 없습니다.
  • Cloud storage explorer 추가: 읽기 전용 기능이 지원되는 Windows Azure Table 및 BLOB 컨테이너 보기 프로그램이 서버 탐색기 패널을 통하여 제공됩니다. SQL Azure를 데이터베이스 목록에 추가하고 동시에 관리할 수 있어 편리합니다.
  • 통합 배포 환경: 솔루션 탐색기에서 Windows Azure 프로젝트를 오른쪽 버튼으로 클릭하여 'Publish' 메뉴를 클릭하면 이제는 직접 배포가 가능합니다. 이전 1.0 및 1.1 버전의 경우, CSPKG 파일과 CSCFG 파일이 생성된 디렉터리의 폴더가 Windows 탐색기에서 열리고, Windows Azure Portal Web Site가 웹 브라우저로 열렸기 때문에, 브라우저에서 로그인하고 여러 단계에 걸쳐서 배포할 파일을 찾아가야했기 때문에 Deploy 절차가 복잡했습니다. 통합 배포 환경에 대해서는 본 강좌의 말미에 한 번 다룰 수 있도록 하겠습니다.
  • 서비스 모니터링: 서비스의 상태를 웹 제어판이 아닌 "서버 탐색기"의 compute 노드에서 실시간으로 모니터링할 수 있습니다.
  • IntelliTrace 기능 지원: Visual Studio 2010을 사용하는 경우, IntelliTrace 기능을 Windows Azure 서비스에 배포한 이후에 "직접" 사용할 수 있습니다. 이 기능은 앞에서 설명한 통합 배포 환경의 일부로 동작하며, 배포가 끝난 이후에 자동으로 이 기능이 시작되어 서비스 모니터링과 함께 실시간으로 로그가 모니터링됩니다.

그간 Windows Azure 기반의 개발이 어려우셨다면 이제는 최신 버전의 SDK와 개발 도구를 이용하여 좀 더 편안하게 Visual Studio 2010을 통해 클라우드 기반 응용프로그램 개발을 시작해보세요. 이러한 기능들은 모두 Windows Azure가 제공하는 Management REST API를 기반으로 작성된 것이며, REST API의 Specification들은 모두 문서화되어있으므로, 누구나 쉽게 응용할 수 있습니다.

참고로 Visual Studio 2010의 데이터베이스 탐색기는 SQL Azure 데이터베이스에 대한 탐색도 지원하므로 이전처럼 별도의 SSMSE를 필요로 하지 않습니다. 그리고 이번 버전의 SDK 역시 1.0과 1.1과 마찬 가지로 처음 설치하는 경우에는 OS와 개발 도구의 환경에 따라 패치와 핫 픽스들을 적용하셔야 합니다.

참고로, IntelliTrace 기능은 Visual Studio 2010 Ultimate에서만 사용이 가능합니다. Visual Web Developer 2010 Express에서는 기본적인 Deploy 기능만이 지원됩니다.

다운로드 바로가기

Welcome to Parallel C#(7) - Excuse me.

C# Parallel Programming 2010. 6. 14. 09:00 Posted by 알 수 없는 사용자

- 뭐, 미안하다고?

선진국에 가보면, 약간만 부딛힐 듯한 상황이라면, 서로 '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

Asynchronous Agents Library – agent. 2 ( 기능 )

VC++ 10 Concurrency Runtime 2010. 6. 13. 09:00 Posted by 알 수 없는 사용자

Asynchronous Agents Library – agent. 2 ( 기능 )

작성자: 임준환( mumbi at daum dot net )

 

void run();

Agent 클래스를 상속 받아 작업을 하는 agent 를 만들 때, 처음으로 해야 할 일은 run() 재정의입니다.

 run() 는 CPU 가 해당 agent 스레드의 컨텍스트를 처리할 때, 수행되는 메소드입니다. 즉, 바로 agent 가 책임지고 처리해야 할 작업( task )이고, run() 을 재정의하기 위해 agent class 가 존재한다고 해도 과언이 아닐 정도로 중요합니다.

 Asynchronous Agents Library( 이하 AAL )을 사용하지 않고 Win32 API 로 직접 스레드를 생성할 때, 지정하는 콜백 함수와 같은 역할을 합니다.

 run() 에 필요한 정보( 매개 변수 )가 있다면 agent 를 상속 받은 클래스의 생성자를 이용하여 전달하면 됩니다.

 run() 이 호출될 때, agent 의 상태는 agent_started 가 됩니다.

 run() 을 재정의할 때, 주의할 점은 run() 이 끝나기 전에 done() 을 호출해야 한다는 것입니다. 실제로 run() 이 끝났다는 것은 작업이 끝난 것이지만 상태는 여전히 agent_started 이기 때문에 계속 수행 중인 것으로 인식됩니다. 그러므로 agent 의 상태를 바꿔주기 위해 반드시 run() 이 끝나기 전에 done() 을 호출해야 합니다.

또한 run() 은 어떤 예외도 던지지 않습니다.

 

bool done();

Agent 의 작업이 완료되었음을 알립니다. 이것은 agent 의 상태를 agent_done 으로 바꾸는 것을 의미합니다.

제대로 agent_done 상태가 되면 true 를 반환합니다. cancel() 에 의해 agent_cancel 상태인 agentagent_done 상태가 되지 않고 false 를 반환합니다.

 protected 로 지정되어 있어 메소드 내에서만 호출할 수 있습니다.

 

bool start();

 start() 를 호출함으로써 CPU 스케줄에 의해 run() 이 호출되는 것입니다. run() 이 호출되기 위해서는 반드시 start() 를 호출해야 합니다. 직접 run() 을 호출하면 병렬 처리 또는 비 동기 처리되지 않고, 호출한 스레드의 컨텍스트에서 일반 함수를 호출한 것과 같게 됩니다.

그러므로 직접 run() 을 호출하는 일은 없어야 하며, 꼭 start() 를 호출하도록 해야 합니다.

 start() 는 agent 의 상태를 agent_created 에서 agent_runnable 로 바꿉니다. 즉, 스케줄하여 컨텍스트 스위칭( context switching ) 의 대상이 되도록 합니다.

Agent 가 제대로 스케줄 되었다면 true 를 반환합니다. 스케줄 되기 전( start() 호출 전 )에 cancel() 을 호출하면 스케줄 되지 않고 false 를 반환합니다.

 

bool cancel();

Agent 객체의 작업을 취소할 때 사용합니다.

Agent 객체가 생성되어 agent_created 상태가 되거나, start() 에 의해 agent_runnable 상태일 때에 작업을 취소하고 종료된 상태인 agent_cancel 상태로 바꿉니다.

다시 말해, run() 이 호출되어 agent_started 상태에서는 agent_cancel 상태로 바뀌지 않고 실패하여 false 를 반환합니다. 제대로 agent_cancel 상태로 바뀌었다면 true 를 반환합니다.

 

agent_status status();

Agent 객체의 현재 상태를 반환합니다.

 agent_statusenum 형으로 agent_canceled( 취소됨 ), agent_created( 생성됨 ), agent_done( 작업 완료 ), agent_runnable( 스케줄 됨 ), agent_started( 실행 중 ) 를 나타냅니다.

반환된 상태는 동기화되어 정확한 상태를 반환하지만, 반환하자마자 agent 의 상태가 변할 수 있어 반환된 상태가 현재 agent 의 상태라고 확신하면 안 됩니다.

 

ISource<agent_status> * status_port();

 status() 는 동기화된 agent 의 상태를 반환하는 반면, status_port() 는 비 동기 메커니즘은 message 를 통해 반환됩니다.

반환형인 ISource 는 message 메커니즘의 interface 입니다. ISource interface 형은 receive() 로 내용을 꺼내올 수 있습니다.

아직 message 에 대해서 언급하지 않았기 때문에 이해가 안 될 수 있습니다. 곧 message 에 대해서 설명할 것인데 그 때, 이 함수가 어떻게 동작하는지 알 수 있을 것입니다.

 

static agent_status wait( agent * _PAgent, unsigned int _Timeout = COOPERATIVE_TIMEOUT_INFINITE );

Win32 API 의 WaitForSingleObject() 와 같은 기능을 합니다.

인자로 넘긴 agent 의 작업이 종료 상태가 될 때까지 기다립니다. 종료 상태란 agent_cancel, agent_done 상태를 말합니다.

기다릴 최대 시간( timeout )을 정할 수 있는데 COOPERATIVE_TIMEOUT_INFINITE 는 무한대를 의미하며, 기본 값으로 지정되어 있습니다. 이 때, 최대 시간은 밀리 초( millisecond ) 단위입니다.

최대 시간을 정했을 경우, 최대 시간까지 agent 의 작업이 종료되지 않으면, operation_timed_out 예외가 발생합니다. 그러므로 최대 시간을 고려한 프로그래밍을 할 경우, 이 예외를 처리함으로써 최대 시간 이 후를 제어해야 합니다.

Agent 의 상태를 반환하는데, 이 상태는 agent_cancel 또는 agent_done 입니다. 왜냐하면 앞의 두 상태 중 하나가 되어야만 반환을 하기 때문입니다.

 

static void wait_for_all( size_t _Count, __in_ecount(_Count) agent ** _PAgents, __out_ecount_opt(_Count) agent_status * _PStatus = NULL, unsigned int _Timeout = COOPERATIVE_TIMEOUT_INFINITE );

Win32 API 의 WaitForMultipleObjects() ( 3번째 인자가 TRUE 인 )와 같은 기능을 합니다.

인자로 전달되는 agent 배열이 모두 완료되었을 때까지 기다립니다.

인자를 살펴보면,

  • _Count - 기다릴 agent 의 개수( 뒤의 인자인 agent 배열의 원소 수와 같아야 합니다. )
  • _PAgents - 기다릴 agent 들의 배열
  • _pStatus – 이 함수가 반환될 때, agent 들의 상태들을 저장할 배열( _Timedout 을 지정했을 때, operation_timed_out 예외가 발생한다면, 상태를 저장하는 작업은 수행되지 않습니다. ), 기본 값은 NULL 입니다.
  • _Timeout – 기다릴 최대 시간, 기본 값은 COOPERATIVE_TIMEOUT_INFINITE 이다.

 wait() 와 마찬가지로 기다릴 최대 시간을 지정할 경우, operation_timed_out 예외가 발생되며, 최대 시간을 고려할 경우, 이 예외를 처리해야 합니다.

 

static void wait_for_one( size_t _Count, agent ** _PAgents, agent_status& _Status, size_t& _Index, unsigned int _Timeout = COOPERATIVE_TIMEOUT_INFINITE );

Win32 API 의 WaitForMultipleObjects() ( 3번째 인자가 FALSE 인 )와 같은 기능을 합니다.

인자로 전달되는 agent 배열 중 하나라도 완료가 될 때까지 기다립니다.

인자를 살펴보면,

  • _Count – 기다릴 agent 의 개수( 뒤의 인자인 agent 배열의 원소 수와 같아야 합니다. )
  • _PAgent – 기다릴 agent 들의 배열
  • _Status – 반환될 때의 agent 상태를 저장할 agent_status 변수
  • _Index – agent 배열 중 완료된 agent 의 인덱스를 저장할 변수
  • _Timeout – 기다릴 최대 시간, 기본 값은 COOPERATIVE_TIMEOUT_INFINITE 입니다.

 wait() 와 마찬가지로 기다릴 최대 시간을 지정할 경우, operation_timed_out 예외가 발생하며, 최대 시간을 고려할 경우, 이 예외를 처리해야 합니다.

 

예제

아래의 코드는 위에 설명한 메소드들을 사용하는 상황을 보여줍니다. 특별한 시나리오는 없지만, 충분히 어떤 상황에 어떻게 사용하는지 알 수 있을 것입니다.

 TestAgentrun() 안에서 사용된 Concurrency::wait() 는 Win32 API 의 Sleep() 과 같은 기능을 합니다. agent::wait() 와 혼동하지 않기를 바랍니다.

예제를 위한 준비

#include <iostream>
#include <agents.h>

using namespace std;
using namespace Concurrency;

const wchar_t* GetAgentStatusString( agent_status status )
{
	switch( status )
	{
	case agent_created:
		return L"agent_created";

	case agent_runnable:
		return L"agent_runnable";

	case agent_started:
		return L"agent_started";

	case agent_done:
		return L"agent_done";

	case agent_canceled:
		return L"agent_canceled";

	default:
		return L"unknown";
	}
}

class TestAgent
	: public agent
{
private:
	unsigned int	id;

protected:
	virtual void run()
	{
		Concurrency::wait( this->id * 500 );		

		wcout << L"agent id: " << this->id << L" completed." << endl;

		done();
	}
public:
	TestAgent( unsigned int id )
		: id( id ) { }
};

[ 코드1. 예제를 위한 코드 ]

status_port() 의 사용

int main()
{	
	TestAgent testAgent( 5 );

	wcout << GetAgentStatusString( receive( testAgent.status_port() ) ) << endl;

	testAgent.start();
	
	for( unsigned int i = 0; i < 10; ++i )
	{		
		wcout << GetAgentStatusString( receive( testAgent.status_port() ) ) << endl;
	}

	agent::wait( &testAgent );

	wcout << GetAgentStatusString( receive( testAgent.status_port() ) ) << endl;
}

[ 코드2. status_port() 사용 예제 ]

 status() 와 같은 기능을 하지만 비 동기 메커니즘인 message 을 사용한다는 것이 다릅니다.

이 말은 status() 가 수행되는 동안 호출한 컨텍스트가 멈추어 있지만, status_port() 는 호출한 컨텍스트는 계속 진행되고, 다른 work 스레드의 컨텍스트가 상태를 처리하고, 그 결과를 message 로 받는 다는 말입니다.

[ 그림1. status_port() 사용 예제 실행 결과 ]


wait()의 사용

int main()
{
	TestAgent testAgent1( 3 );
	TestAgent testAgent2( 2 );
	TestAgent testAgent3( 1 );
	TestAgent testAgent4( 5 );
	TestAgent testAgent5( 4 );

	testAgent1.start();
	testAgent2.start();
	testAgent3.start();
	testAgent4.start();
	testAgent5.start();

	agent::wait( &testAgent1 );
}

[ 코드3. wait() 사용 예제 ]

각기 다른 5 개의 작업들을 수행하는 agent 들을 생성합니다. 각각의 생성자의 인자는 각 agent 의 id 이기도 하지만, 해당 작업들의 가중치이기도 합니다.

testAgent1 의 작업이 끝날 때까지 기다립니다. 곧바로 프로그램이 종료되므로 작업이 끝나지 않은 agent 들은 비정상적으로 종료됩니다. 보통 작업이 진행 중인 모든 agent 들이 완료될 때가지 기다리는 것이 좋습니다.

[ 그림2. wait() 사용 예제 실행 결과 ]


wait() 와 timeout

int main()
{
	TestAgent testAgent1( 3 );
	TestAgent testAgent2( 2 );
	TestAgent testAgent3( 1 );
	TestAgent testAgent4( 5 );
	TestAgent testAgent5( 4 );

	testAgent1.start();
	testAgent2.start();
	testAgent3.start();
	testAgent4.start();
	testAgent5.start();

	try
	{
		agent::wait( &testAgent4, 1000 );
	}
	catch( operation_timed_out& )
	{
		wcout << L"operation timed out." << endl;
	}
}

[ 코드4. wait() 와 timeout 예제 ]

 wait() 의 timeout 매개변수를 사용하려면 operation_timed_out 예외를 처리해야 합니다.

[ 그림3. wait() 와 timeout 예제 실행 결과 ]


wait_for_all() 의 사용

int main()
{
	TestAgent testAgent1( 3 );
	TestAgent testAgent2( 2 );
	TestAgent testAgent3( 1 );
	TestAgent testAgent4( 5 );
	TestAgent testAgent5( 4 );

	testAgent1.start();
	testAgent2.start();
	testAgent3.start();
	testAgent4.start();
	testAgent5.start();

	agent* runningAgents[5] = {
		&testAgent1,
		&testAgent2,
		&testAgent3,
		&testAgent4,
		&testAgent5
	};

	unsigned int runningAgentCount = sizeof( runningAgents ) / sizeof( runningAgents[0] );	

	agent::wait_for_all( runningAgentCount, runningAgents );
}

[ 코드5. wait_for_all() 사용 예제 ]

배열에 포함된 모든 agent 들이 완료될 때까지 기다립니다.

[ 그림4. wait_for_all() 사용 예제 실행 결과 ]


wait_for_all() 과 상태 반환

int main()
{
	TestAgent testAgent1( 3 );
	TestAgent testAgent2( 2 );
	TestAgent testAgent3( 1 );
	TestAgent testAgent4( 5 );
	TestAgent testAgent5( 4 );

	testAgent1.start();
	testAgent2.start();
	testAgent3.start();
	testAgent4.start();
	testAgent5.start();

	agent* runningAgents[5] = {
		&testAgent1,
		&testAgent2,
		&testAgent3,
		&testAgent4,
		&testAgent5
	};

	agent_status statuses[5];

	unsigned int runningAgentCount = sizeof( runningAgents ) / sizeof( runningAgents[0] );
	agent::wait_for_all( runningAgentCount, runningAgents, statuses );

	for( unsigned int i = 0; i < runningAgentCount; ++i )
		wcout << GetAgentStatusString( statuses[ i ] ) << endl;
}

[ 코드6. wait_for_all() 과 상태 반환 예제 ]

완료된 agent 들의 상태를 저장하는 배열의 크기는 agent 들을 포함한 배열의 크기와 같아야 합니다다. 만약 timeout 예외를 사용한다면 상태는 저장되지 않습니다.

[ 그림5. wait_for_all() 과 상태 반환 예제 실행 결과 ]


wait_for_one() 의 사용

int main()
{
	TestAgent testAgent1( 3 );
	TestAgent testAgent2( 2 );
	TestAgent testAgent3( 1 );
	TestAgent testAgent4( 5 );
	TestAgent testAgent5( 4 );

	testAgent1.start();
	testAgent2.start();
	testAgent3.start();
	testAgent4.start();
	testAgent5.start();

	agent* runningAgents[5] = {
		&testAgent1,
		&testAgent2,
		&testAgent3,
		&testAgent4,
		&testAgent5
	};

	agent_status finalStatus;
	unsigned int indexOfcompletedAgentFirst;

	unsigned int runningAgentCount = sizeof( runningAgents ) / sizeof( runningAgents[0] );	
	agent::wait_for_one( runningAgentCount, runningAgents, finalStatus, indexOfcompletedAgentFirst );

	wcout << L"final status : " << GetAgentStatusString( finalStatus ) << endl;
	wcout << L"index of completed agent first : " << indexOfcompletedAgentFirst << endl;	
}

[ 코드7. wait_for_one() 사용 예제 ]

여러 agent 들 중 하나라도 완료 되었을 때 반환됩니다. 상태 저장을 위한 변수와 인덱스 저장을 위한 변수는 참조로 넘겨야 하므로 생략할 수 없습니다.

[ 그림6. wait_for_one() 사용 예제 실행 결과 ]


 

마치는 글

위의 내용을 모두 숙지했다면 agent 를 마치 하나의 work 스레드처럼 다룰 수 있을 것입니다. 이렇게 하여 비 동기 병렬 처리를 쉽게 처리할 수 있습니다.

하지만 이것이 끝이 아닙니다. agent 에 message 메커니즘을 적용한다면 더욱 더 지능적인 agent 를 쉽게 만들 수 있습니다.

다음 글에서는 message 메커니즘에 대해 소개해 보도록 하겠습니다.

Welcome to Parallel C#(6) - To be continue...

C# Parallel Programming 2010. 6. 10. 09:00 Posted by 알 수 없는 사용자

- 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