지난 글 unbounded_buffer 에 이어 또 다른 message block 인 overwrite_buffer 와 single_assignment 에 대해서 알아보도록 하겠습니다.
Message block 들은 특징들이 모두 다르기 때문에 지난 unbounded_buffer 을 생각하시면 이해가 어려울 수 있습니다. message block 하나 하나의 쓰임새가 다르므로 새로운 것을 알아본다고 생각하시는 것이 좋을 것 같습니다.
overwrite_buffer< _Type >
지금부터 설명드릴 overwrite_buffer 은 unbounded_buffer 와는 달리 하나의 변수라고 생각하시면 이해하기 쉬울 것입니다.
Concurrency runtime 을 사용하지 않고, 스레드 간의 상태나 정보를 공유하려면 전역 변수나 힙( heap ) 에 할당된 변수에 락( lock ) 을 걸어 사용해야 합니다.
overwrite_buffer 는 방금 언급한 번거로운 작업들을 알아서 해줍니다. 내부에서 힙에 메모리를 할당하고, 접근 시 락을 겁니다. 하지만 사용하는 우리는 그런 것들을 신경 쓰지 않고 마치 지역 변수처럼 사용할 수 있습니다.
unbounded_buffer 는 외부에서 message 를 받아가면 내부에서 해당 message 가 제거되는 반면에, overwrite_buffer 는 제거되지 않습니다. 또한 하나의 변수와도 같기 때문에 외부에서 message 를 보내면 이 전의 message 를 덮어쓰고 새 message 가 저장됩니다.
결국 overwrite_buffer 는 단 하나의 message 만을 갖게 됩니다.
그럼 overwrite_buffer 의 멤버 함수에 대해서 알아보도록 하겠습니다.
멤버 함수
생성자와 소멸자를 제외한 public 인 멤버 함수들입니다.
bool has_value() const
현재 message 를 가지고 있는지 반환합니다.
어떠한 message 도 갖지 않을 경우에 false 를 반환합니다. 만약, 한번이라도 overwrite_buffer 에 message 가 전달된다면 그 후부터는 true 를 반환합니다. overwrite_buffer 는 외부에서 message 를 받아가도 내부의 message 가 제거되지 않기 때문입니다.
Message 를 갖고 있지 않을 때, 외부에서 동기 함수인 receive() 를 사용해 message 를 얻기를 원한다면 overwrite_buffer 에 message 가 들어올 때까지 기다립니다. message 가 제거되지 않기 때문에 한번이라도 overwrite_buffer 가 message 를 받으면 receive() 가 기다리는 일은 없을 것입니다.
_Type value();
현재 가지고 있는지 message 를 반환합니다.
내부적으로 동기 전달 함수인 receive() 를 사용하므로 message 를 가지고 있지 않다면 message 를 갖게 될 때까지 기다립니다. 만약 이 때, has_value() 를 호출했다면 false 를 반환할 것입니다.
Message 가 제거되지 않기 때문에 전달 함수를 이용해 message 를 받아갈 경우, 복사본이 전달됩니다.
예제
overwrite_buffer 의 간단한 예제를 구현해보도록 하겠습니다.
시나리오
네트워크 지연 시간을 갱신하고, 출력하는 프로그램을 작성할 것입니다.
네트워크 지연 시간을 갱신하는 역할을 하는 agent 와 갱신된 정보를 출력하는 agent 가 하나의 overwrite_buffer 를 공유하여 사용하는 예제입니다.
코드
#include
#include
#include
using namespace std;
using namespace Concurrency;
// 지연 시간을 얻어오는 agent.
class PingUpdater
: public agent
{
public:
PingUpdater( const array< unsigned int, 5 >& delayTimeSource, ITarget< unsigned int >& targetBlock )
: delayTimeSource( delayTimeSource )
, targetBlock( targetBlock ) { }
protected:
// 2초마다 지연 시간을 얻어 옴.
void run()
{
while( true )
{
asend( this->targetBlock, this->GetDelayTime() );
Concurrency::wait( 2000 );
}
this->done();
}
// 지연 시간을 시뮬레이션하는 함수.
unsigned int GetDelayTime()
{
static unsigned int index = 0;
unsigned int delayTime = this->delayTimeSource[ index ];
if( index + 1 < this->delayTimeSource.size() )
++index;
else
index = 0;
return delayTime;
}
private:
const array< unsigned int, 5 >& delayTimeSource;
ITarget< unsigned int >& targetBlock;
};
// 지연 시간을 출력하는 agent.
class PingDisplayer
: public agent
{
public:
PingDisplayer( ISource< unsigned int >& sourceBlock )
: sourceBlock( sourceBlock ) { }
protected:
// 1초마다 지연 시간을 출력한다.
void run()
{
while( true )
{
this->Display( receive( this->sourceBlock ) );
Concurrency::wait( 1000 );
}
this->done();
}
// 지연 시간을 출력하는 함수.
void Display( unsigned int delayTime )
{
wcout << L"current delay time: " << delayTime << endl;
}
private:
ISource< unsigned int >& sourceBlock;
};
int main()
{
// 네트워크 지연 시간의 시뮬레이션 정보.
array< unsigned int, 5 > delayTimeSource = { 210, 211, 261, 246, 223 };
// 공유 버퍼
overwrite_buffer< unsigned int > delayTimeBuffer;
// 네트워크 지연 시간을 갱신하는 agent 와 출력하는 agent.
PingUpdater updater( delayTimeSource, delayTimeBuffer );
PingDisplayer displayer( delayTimeBuffer );
// agent 시작.
updater.start();
displayer.start();
// agent 의 작업이 모두 끝날 때까지 대기.
agent* waitingAgents[2] = { &updater, &displayer };
agent::wait_for_all( 2, waitingAgents );
}
[ 코드1. overwrite_buffer 를 이용한 네트워크 지연 시간 갱신 및 출력 예제 ]
PingUpdater 클래스는 agent 클래스로 네트워크 지연 시간을 갱신하는 역할을 합니다. 2초에 한 번씩 시뮬레이션을 위해 준비된 정보를 순회하며 얻어와서 overwrite_buffer 에 전달합니다.
PingDisplayer 클래스도 agent 클래스로 갱신된 네트워크 지연 시간을 화면에 출력하는 역할을 합니다. 1초에 한번씩 overwrite_buffer 로부터 갱신된 정보를 가져와서 화면에 출력합니다.
예제에서 사용된 Concurrency::wait() 는 Win32 API 의 Sleep() 과 같은 역할을 합니다. agent::wait() 과 혼동하지 않길 바랍니다.
위 코드를 보시면 굉장히 직관적이고, 간단하게 멀티 스레드 프로그래밍을 할 수 있다는 것을 알 수 있을 것입니다.
[ 그림1. overwrite_buffer 를 이용한 네트워크 지연 시간 갱신 및 출력 예제 ]
single_assignment< _Type >
single_assignment 는 위에서 설명한 overwrite_buffer 와 거의 흡사합니다.
단지 다른 점이 있다면 message 를 한번만 받을 수 있다는 것입니다. 만약 두 번 이상 보낸 다면 두 번째부터는 무시됩니다.
멤버 함수 또한 거의 같지만, 다른 점을 알아보겠습니다.
멤버 함수
생성자와 소멸자를 제외한 public 인 함수들입니다.
bool has_value() const
위에서 설명한 overwrite_buffer 의 has_value() 와 같습니다.
Message 를 단 한번도 받지 않았다면 false 를 반환하고, 받았다면 true 를 반환합니다.
_Type const & value()
overwrite_buffer 의 value() 와 같은 기능을 합니다.
하지만 값을 반환하지 않고 const 참조를 반환한다는 것이 다릅니다.
overwrite_buffer 의 value() 와 마찬가지로 message 를 갖고 있지 않다면 message 를 갖게 될 때까지 기다립니다.
마치는 글
이번 글에서는 overwrite_buffer 와 single_assignment 에 대해서 알아보았습니다.
single_assignment 는 overwrite_buffer 와 거의 흡사하기 때문에 예제는 생략하였습니다.
지난 글에서 본 unbounded_buffer 와는 분명히 쓰임새가 다르므로 특징을 잘 파악해두시면 좋을 것입니다.
다음 글에서 또 다른 message block 을 소개해드릴 것입니다. 그 message block 또한 쓰임새가 분명히 다르므로 Asynchronous Agents Library 의 활용도가 굉장히 넓다는 것을 아시게 될 것입니다.
주요 변경 사항으로 소개되 내용에서, unique_ptr에 대해서 다음과 같이 설명하고 있습니다.
"auto_ptr 클래스보다 더 안전한 스마트 포인터형인 unique_ptr 클래스의 구현에도 Rvalue reference가 사용되었습니다. unique_ptr 클래스는 move는 할 수 있지만 copy는 불가능하며, safety에 영향을 미치지 않으면서 강한 소유 의미(strict ownership semantics)를 구현했습니다. 또한, unique_ptr 클래스는 rvalue references가 구현된 container들과 잘 동작합니다."
unique_ptr in MSDN Library
MSDN Library에 소개된unique_ptr class에 관한 설명입니다.
- 소유하는 객체에 대한 포인터를 저장한다. 해당 객체는 다른 unique_ptr에 의해서 소유될 수 없다. 해당 객체는 unique_ptr이 해제될 때에 해제된다. 다음은 MSDN에 게시되어 있는 class 정의 코드입니다.
왜 auto_ptr은 deprecation이 되었을까? C++ 0x에서 auto_ptr은 deprecation으로 결정되었습니다. C++ 0x에서는 최소한 auto_ptr과 같은 효율을 가지며 move semantics를 지워하는 unique_ptr을 추가하게 됩니다. 그렇다면 auto_ptr은 왜 deprecation이 되었을까요? 그 이유에 대해서 설명드리겠습니다.
역사적으로 이전 버전의 C++ 표준에서, 반복된 수정과 패치를 통해서 주요 확장에서 auto_ptr은 안정성을 확보했습니다. 하지만 설계상의 심각한 결함을 극복하지는 못했습니다. 대표적으로 generic 알고리즘과 auto_ptr을 함께 사용할 때에 문제가 되었습니다. generic 알고리즘에서는 복사 연산 문법은 정말로 copy 연산이 일어난다는 것을 가정합니다. 하지만 auto_ptr에서 복사 연산자는 실제로 move와 같이 동작합니다. 이런 근본적인 차이 때문에 알고리즘의 구현에 따라서 원하는 결과를 얻지 못 할 수도 있었습니다.
정렬 알고리즘을 통해서 예를 들겠습니다. 버블 정렬이나 선택 정렬에서는 하나의 지역변수에 한 원소를 골라서 복사를 하는 방식을 사용합니다. 이런 경우에는 완벽하게 유효한 구현이 됩니다. 하지만 빠른 정렬의 경우를 생각해보면, 구현 중에 다음과 같은 코드가 있을 겁니다.
value_type pivot = *mid_point;
이 경우에 알고리즘상 복사 연산자에서 실제로 복사가 일어남을 전제하는 알고리즘인 겁니다. 하지만 auto_ptr은 복사 표현을 통해서 move를 구현했기 때문에 이런 구현에서는 sort()의 결과가 안전할 것인지 보장 할 수가 없었습니다.
수년간의 논의 끝에, C++ 표준 위원회는 auto_ptr은 deprecated로 결정했습니다. 하지만 auto_ptr의 다른 모든 장점을 수용할 수 있으며 더 안전하고 명확한 인터페이스를 C++ 0x에 추가 될 필요가 있었고, 그 대채자가 unique_ptr 입니다. unique_ptr은 복사 생성자를 private으로 선언합니다. 따라서 generic 알고리즘을 unique_ptr과 같이 사용하여 호출할 때에, 모든 generic 코드는 컴파일 타임에 에러가 나거나 그렇지 않다면 완벽하게 유효한 결과를 내게 됩니다.
다음 글에서는 unique_ptr의 예제 코드 위주로 auto_ptr과의 차이점과 개선점을 살펴보도록 하겠습니다.
첫 번째 방법에서 std::string을 사용한 이유는 다름이 아니고 메모리 확보 때문입니다.
마샬링을 통해서 char*와 wchar_t*에 메모리 주소를 저장합니다. 문자열 그 자체를 복사하는 것이 아닙니다. 그래서 변환한 문자열을
저장할 메모리 주소를 확보하고 사용 후에는 해제를 해야 합니다. 메모리 확보와 해제를 위해서 marshal_context를 사용합니다.
marshal_context는 변환에 필요한 메모리를 확보하고, 스코프를 벗어날
때 메모리를 해제합니다.
const char*
s2;
const
wchar_t* s3;
{
marshal_context ctx;
s2 = ctx.marshal_as<const
char*>(s0);
s3 = ctx.marshal_as<const
wchar_t*>(s0);
}
String^을 C/C++ 문자열로 변환할 때는 std::string + marshal_as 나 marshal_context 둘
중 하나를 선택하여 사용합니다.
이전 글에서 message block 의 인터페이스인 ISource 와 ITarget 인터페이스에 대해서 알아보았습니다. 이번 글부터 그 인터페이스들을 상속받아 구현한 message block 에 대해 알아보겠습니다.
Message block 은 버퍼( buffer ) 를 가질 수도 있고, 상태만 가질 수도 있고, 기능만 가질 수도 있습니다. 그러므로 각 message block 들의 특징을 잘 파악하고, 언제 필요한지 알아야 합니다.
이번 글에서는 가장 범용적인 유용한 unbounded_buffer 에 대해서 알아보도록 하겠습니다.
unbounded_buffer< _Type >
unbounded_buffer 는 message block 중 가장 많이 사용될 것입니다. unbounded_buffer 는 내부적으로 큐( queue )를 구현하고 있어 message 저장소 역할을 합니다. 누군가 unbounded_buffer 에 message 를 보내면 unbounded_buffer 에 순서대로 차곡차곡 쌓이고, 쌓인 순서대로 꺼내서 쓸 수 있습니다. 꺼낸 message 는 unbounded_buffer 에서 제거됩니다. 그렇기 때문에 여러 곳에서 같은 message 를 꺼내 받을 수 없습니다.
이런 작업들은 비 동기 agent 들과 사용할 때, 빛을 발합니다.
unbounded_buffer 는 스레드에 안전하므로 직접 lock 을 하지 않아도 됩니다.
생성자
unbounded_buffer() – 기본 생성자
빈 unbounded_buffer 를 생성합니다.
unbounded_buffer( filter_method const& _Filter )
빈 unbounded_buffer 를 생성합니다. 하지만 필터 함수를 지정하여 받을 수 있는 메시지를 거를 수 있습니다.
이 필터 함수는 bool (_Type const &) 형의 시그니처( signature )를 갖습니다.
멤버 함수
bool enqueue( _Type const& _Item )
하나의 message 를 unbounded_buffer 에 보냅니다.
message 전송이 성공이면 true, 아니면 false 를 반환합니다.
내부적으로 이 함수는 message 전달 함수인 send() 를 사용합니다. 그리고 send() 의 결과를 반환합니다.
_Type dequeue()
unbounded_buffer 에서 하나의 message 를 꺼냅니다. 꺼낸 message 는 큐에서 제거됩니다.
꺼내진 message 를 반환합니다. 그리고 꺼내진 message 는 unbounded_buffer 내부에서 제거됩니다.
enqueue() 와 마찬가지로 내부적으로 message 전달 함수인 receive() 를 사용합니다. receive() 가 반환한 값을 반환합니다.
예제
unbounded_buffer 를 사용하여 작은 시나리오를 구현해보도록 하겠습니다.
시나리오
윈도우즈 OS 는 사용자의 이벤트들을 메시지 큐에 담고, 큐에 들어온 메시지들을 순차적으로 꺼내서 처리하는 메커니즘을 사용합니다. 이 시나리오를 agent 와 unbounded_buffer 를 이용하여 간단하게 구현해보겠습니다.
코드
#include
#include
#include
using namespace std;
using namespace Concurrency;
// 메시지 객체
class Message
{
wstring message;
public:
Message( const wstring& message )
: message( message ) { }
const wstring& GetMessage() const
{
return this->message;
}
};
// 메시지를 발생하는 사용자 agent
class User
: public agent
{
ITarget< Message >& messageQueue;
public:
User( ITarget< Message >& target )
: messageQueue( target ) { }
void ClickMouseLButton()
{
send( this->messageQueue, Message( L"WM_LBUTTONDOWN" ) );
send( this->messageQueue, Message( L"WM_LBUTTONUP" ) );
}
void DragMouseLButton()
{
send( this->messageQueue, Message( L"WM_LBUTTONDOWN" ) );
send( this->messageQueue, Message( L"WM_MOUSEMOVE" ) );
send( this->messageQueue, Message( L"WM_LBUTTONUP" ) );
}
virtual void run()
{
this->ClickMouseLButton();
Concurrency::wait( 1000 );
this->DragMouseLButton();
this->done();
}
};
// 발생한 메시지들을 처리하는 메시지 펌프 agent
class MessagePump
: public agent
{
ISource< Message >& messageQueue;
public:
MessagePump( ISource< Message >& source )
: messageQueue( source ) { }
void ProcessMessage( const Message& message )
{
wcout << message.GetMessage() << endl;
}
virtual void run()
{
while( true )
{
Message message = receive( this->messageQueue );
this->ProcessMessage( message );
}
this->done();
}
};
int main()
{
// 메시지 큐
unbounded_buffer< Message > messageQueue;
// 메시지를 발생하는 사용자와 메시지 펌프
User user( messageQueue );
MessagePump messagePump( messageQueue );
// agent 시작.
user.start();
messagePump.start();
// agent 의 작업이 모두 끝날 때까지 대기
agent* agents[] = { &user, &messagePump };
agent::wait_for_all( 2, agents );
}
[ 코드1. agent 와 unbounded_buffer 를 이용한 메시지 펌프 간략 구현 ]
Message 클래스는 단순히 문자열을 래핑( wrapping ) 하는 클래스로 큐에 저장되는 메시지를 나타냅니다.
agent 로 2개를 정의하였는데 하나는 사용자가 이벤트 메시지를 발생하는 것을 흉내 낸 User 클래스이고, 다른 하나는 메시지 펌프를 간략화한 MessagePump 클래스입니다.
사용자 agent( User 객체 )는 약간의 시간차를 두고 이벤트 메시지를 발생합니다. 발생된 메시지는 메시지 큐에 저장됩니다.
메시지 펌프 agent 는 메시지가 저장될 때까지 대기하다가 메시지가 저장되면 그 메시지를 받아서 처리합니다. 처리된 메시지는 메시지 큐에서 제거됩니다.
agent 들의 start() 를 사용하여 작업을 시작하고, 모든 agent 의 작업이 끝날 때까지 기다립니다.
실제로는 하나의 agent 가 무한 루프를 수행하므로 프로그램이 종료되지 않습니다.
위의 예제처럼 데이터를 보내고, 순차적으로 받아서 처리하고 싶을 때, 내부적으로 큐가 필요할 때 유용한 message block 이 바로 unbounded_ buffer 입니다.
멀티 스레드 프로그래밍 시 자료 구조 중 큐가 많이 사용되기 때문에 unbounded_buffer 도 많이 사용하게 될 것입니다.
[ 그림1. agent 와 unbounded_buffer 를 이용한 메시지 펌프 간략 구현 결과 ]
마치는 글
이번 글에서는 message block 중 가장 사용도가 높은 unbounded_buffer 에 대해서 알아보았습니다. unbounded_buffer 이외에도 다양한 message block 이 있습니다.
다음 글에서는 overwrite_buffer 라는 message block 에 대해서 알아보도록 하겠습니다.
setlocale로 국가를 설정하고(직접 나라를 지정할 수도 있고, 아니면 위처럼 시스템 설정에 따라가도록 할 수도 있습니다), ‘cout’ 대신
‘wcout’를 사용합니다.
관리코드 문자열과 비관리코드 문자열간의 변환에
따른 성능
C++로 만드는 프로그램은 보통 고성능을 원하는 프로그램이므로 보통 C++ 프로그래머는 성능에 민감합니다. 마샬링은 공짜가 아닙니다만
많은 양을 아주 빈번하게 마샬링 하는 것이 아니면 성능에 너무 신경 쓰지 않아도 됩니다. 다만 기본적으로
관리코드의 문자열은 유니코드입니다. 그래서 비관리코드의 문자열이
ANSI 코드라면 유니코드를 사용했을 때 보다 더 많은 시간이 걸립니다(정확한 수치는 잘
모르지만 ANSI가 유니코드보다 3배정도 더 걸린다고도 합니다). 그래서 관리코드와 비관리코드를 같이 사용할 때는 가능한 유니코드를 사용하는 것이 훨씬 좋습니다.
지난 포스팅에서는 드디어 WM_TOUCH를 이용한 멀티터치 UX의 구현방법에 대한 이야기가 시작되었습니다. channel9에 공개된 예제를 함께 작성해 보면서 WM_TOUCH 메세지를 다루는 방법을 알아보고 있는데요, 어떨 때 WM_TOUCH를 이용해야 하는지, WM_GESTURE가 아닌 WM_TOUCH를 받고 싶을 땐 어떻게 처리해 주어야 하는지, 그리고 메세지를 받을 때마다 호출되는 CWnd::OnTouchInput 등에 대한 설명이 있었습니다.
그리고 실제로 OnTouchInput() 함수에서 터치 메세지를 제어할 예제 프로그램인 ‘TouchPad’ 프로젝트를 생성하고 기본적인 전처리 단계를 알아보았지요. 이에 이어서 오늘은 OnTouchInput() 함수의 구현부터 본격적으로 알아보도록 하겠습니다 ^^
Task 3 : 드로잉 코드는 가져다 씁시다. 터치에 집중해야죠 ㅎㅎ
지금 함께 작성해 보려고 하는 TouchPad는 윈도우 그림판처럼 멀티 터치 입력에 대해 라인을 그려주는 예제입니다. 한 발 더 나가서, 서로 다른 입력으로 드로잉 하는 라인은 색상도 다르게 표시되도록 할거예요. 하지만 사용자로부터 입력된 터치 정보의 처리방법에 집중하기 위해, 드로잉 코드는 샘플에서 제공된 클래스를 사용하도록 하겠습니다. 먼저 예제 프로젝트에 아래의 파일을 추가해 주세요.
이 클래스들을 이용해 우리가 구현할 코드에 대해 간략히 설명해 보기로 하지요. 우리는 각각의 터치 입력에 대해서 라인(stroke. 스트로크라고 부르겠습니다.)을 그려내는 기능을 추가할 것인데, 이를 이해서 두 개의 스트로크 모음을 유지할겁니다. 한 쪽에서는 이미 드로잉이 끝난 스트로크 데이터를 모아 유지하게 되고, 또 다른 나머지 한 쪽에서는 현재 터치입력으로 드로잉하고 있는 데이터를 유지하게 됩니다. 스크린을 터치하는 한 개 혹은 다수의 입력을 받아 m_StrkColDrawing 멤버변수가 유지하는 스트로크 정보에 CPoint 형식의 좌표 데이터를 추가합니다. 그리고 스크린에서 손가락을 떼면, 손가락을 따라 그려지고 있던 스트로크 정보를 m_StrkColDrawing 변수에서 m_StrkColFinished로 이동시킵니다. 이 때 동시에 다수의 터치 입력이 동시에 일어나면 이를 구분하기 위해 서로가 다른 색상을 가진 스트로크를 출력하게 할겁니다.
그럼 소스코드 상에서의 수정사항을 알아보기로 하지요. 먼저 위에 첨부되어있는 파일들을 다운받아서 프로젝트에 넣은 후, StdAfx.h 헤더파일에 새로 추가된 헤더들을 인클루드 해주세요.
// stdafx.h 파일에 추가해 주세요.
#include"Stroke.h"
#include"StrokeCollection.h"
그리고 ChildView.h 파일로 가서 CChildView 클래스에 private 멤버 변수로 아래의 세 변수를 추가해 줍니다.
private:
int m_iCurrColor; // The current stroke color
CStrokeCollection m_StrkColFinished; // The user finished entering strokes
// after user lifted his or her finger.
CStrokeCollection m_StrkColDrawing; // The Strokes collection the user is
// currently drawing.
이 중에서 m_iCurrColor는 드로잉 되는 스트로크의 색상을 결정하기 위한 인덱스로 쓰일 겁니다. 생성자에서 0으로 초기화 해주세요.
CChildView::CChildView() : m_iCurrColor(0)
{
}
드로잉이 다 끝나서 m_StrkColFinished 변수에 담겨 있을 스트로크 정보들을 화면에 출력해 주기 위해 CChileView::OnPaint() 함수에 아래의 코드를 추가해 줍니다.
// 드로잉이 완료된 스트로크 정보를 출력한다.
m_StrkColFinished.Draw(&dc);
Task 4 : 터치 데이터를 다뤄보자 (드디어!)
이제 드로잉 코드의 기본적인 설정들은 끝을 냈고, 지금부터 우리는 WM_TOUCH 메세지에 대해 아래의 세 가지 경우에 따른 코딩을 추가해 넣을 것입니다.
사용자가 스크린에 터치를 시작한 시점의 처리. – Touch Input Down
터치한 손가락을 스크린 상에서 움직이는 동안의 처리. – Touch Input Move
스크린에서 이동하던 손가락을 떼어 터치 입력이 끝나는 시점의 처리. – Touch Input Up
각각의 경우를 처리하기 위한 코드를 클래스에 추가해 봅시다. ChildView.h 헤더파일에서 위의 세가지 경우에 호출할 CChildView 클래스의 멤버함수를 선언해 줍니다.
각각의 함수 본문에는 터치가 발생한 시점에 걸맞는 드로잉 코드들이 구현되어 있습니다. 샘플 원본에는 영어로 달려있던 주석을 제가 한글로 바꾸고 추가 설명도 좀 덧붙여 두었습니다. 아직까지는 별다른 설명 없이 코드만 보더라도 어렵지 않게 이해하실 수 있을 거예요.
이 함수들의 본문보다 더 중요한 부분은, 이 함수들을 '언제 호출해 주어야 하느냐' 하는 점입니다. 멀티 터치 데이터의 입력 시점에 따른 상태를 확인하기 위해 주의깊게 확인해야 할 부분은 지난 포스팅에서 선언만 해두고 빈 함수 몸통만 덩그라니 남겨두었던 CChildView::OnTouchInput(...) 함수입니다. 이곳에서 현재 멀티터치 데이터를 조회해 현재 입력 시점이 어떻게 되는지, 혹은 부가적인 다른 정보들을 어떻게 얻어내는지를 알아보는 것이 이번 포스팅의 중요한 목표라고 볼 수 있죠. 아래의 코드를 유심히 봐주세요.
BOOL CChildView::OnTouchInput(CPoint pt, int nInputNumber, int nInputsCount, PTOUCHINPUT pInput)
{
if ((pInput->dwFlags & TOUCHEVENTF_DOWN) == TOUCHEVENTF_DOWN)
OnTouchInput 함수의 인자로 TOUCHINPUT 구조체의 포인터가 전달되는데, 이 구조체의 멤버 변수인 dwFlag에 담겨있는 flag의 상태를 조회함으로써 현재 터치 입력에 대한 여러 가지 정보들을 알아낼 수 있습니다. 위에 적힌 코드에서도 dwFlag를 이용해 터치 데이터의 추가 정보들을 알아내고 있습니다. 위의 함수 본문을 복사해 OnTouchInput 함수의 구현을 대체해 주세요.
이 중 일련의 멀티터치 입력이 처음 시작될 때의 처리를 위한 OnTouchInputDown(…) 함수를 한 번 보기로 하죠. 여기에서도 TOUCHINPUT 구조체의 dwFlag 를 조회하는 코드를 볼 수 있습니다. 바로 TOUCHEVENTF_PRIMARY 플래그가 켜져 있는지를 확인해 보는 코드네요. 입력중인 터치 정보가 여러 개인 경우, 이 중 가장 먼저 시작된 (PRIMARY) 터치 정보 인지를 확인하는 부분입니다. PRIMARY 터치는 스트로크의 색상을 검정으로 유지하고, 그렇지 않은 경우는 미리 정의된 몇 가지 색상 정보를 rotation해가며 결정해 주기 위해서 PRIMARY 데이터 여부를 확인하고 있습니다. 스트로크의 색상을 결정해주는 GetTouchColor() 함수의 선언 및 구현도 아래와 같이 추가해 주세요.
이제 추가해야 할 코딩의 마지막 입니다. 드로잉을 하면서 추가해 주었던 스트로크 정보들의 해제 처리를 소멸자에 넣어주세요. 멀티터치 데이터의 조작 방식을 알아보기 위한 이번 예제는 동적 메모리 할당의 효율적인 처리 같은 관점에서는 그다지 효율적이지 못합니다. 그래서 실행해보면 다소 반응 속도가 느리다고 느껴지실 수도 있습니다만, 이번 예제가 어디까지나 MFC 프로젝트에서 WM_TOUCH 메세지의 조작 방식을 알아보기 위함임을 감안해 주세요 ㅎㅎㅎ 그래도 양심상 할당한 메모리의 해제는 해주어야겠지요 ^^;… CChildView의 소멸자에 아래 코드를 넣어주시면 이제 코드 추가는 모두 완료입니다!
CChildView::~CChildView()
{
for (int i = 0; i < m_StrkColDrawing.GetCount(); ++i)
{
delete m_StrkColDrawing[i];
}
for (int i = 0; i < m_StrkColFinished.GetCount(); ++i)
{
delete m_StrkColFinished[i];
}
}
Task 5 : 프로그램을 실행하고 직접 스크린을 ‘멀티’터치해 봅니다.
이제 빌드하고 프로그램을 실행해 봅니다. 스크린 터치를 통해서 view 영역에 동시에 서로 다른 라인이 그려지는 기능을 직접 확인해 보세요. 샘플 프로그램을 훌륭하게 서로 다른 panning 제스처를 인식해 각각의 입력에 따른 스트로크를 출력하게 됩니다.
Outro
이번 글에서는 지난번 포스팅에 이어서 WM_TOUCH 메세지를 사용해 멀티터치 UX를 구현하는 TouchPad 예제를 완성해 보았습니다. WM_TOUCH 메세지가 발생할 때마다 호출되는 CWnd::OnTouchInput() 함수에서
Touch 입력의 시작, 진행 중, 종료 시점을 알아내는 방법
DWORD 타입의 ID값을 이용해 서로 다른 터치 입력을 구분하는 방법
동시에 여러 개의 멀티터치가 발생할 때, 먼저 일어난(primary) 터치 데이터를 구별하는 방법
… 등을 알아보았습니다. 또한 이를 활용해 동시 다발적인 멀티터치 입력에 대해 각기 다른 panning 제스처로 인식해 궤적을 그려내는 방법을 예제 구현과 함께 알아보았습니다. 이를 통해 WM_TOUCH 메세지를 제어하는 방법을 보다 쉽게 이해할 수 있으셨을 겁니다.
Native 코드로 멀티터치 UX를 구현하는 방법 중에 아직 소개해 드리지 않은 영역이 있습니다. MFC로 제공되는 기능은 아니지만, Com-Interface 형태로 제공되는 Manipulation(MS Surface에서와 같은 위젯 처리 기법. 한번에 여러 개의 제스처를 동시 적용한다.), Inertia(관성)의 제어기법 등이 그것입니다. 다음 포스팅에서는 이러한 부분에 대해서 알아보도록 하겠습니다.
Touch 관련 업무에 종사하는 사람으로써 좋은글 잘 읽고 있습니다.
궁금한게 있어서요. 구현하신 touchpad 예제에서 4~5개가 선이 그려진것 같습니다.
그럼 Window 7 에 있는 기본적인 그림판에서도 4~5개가 인식되면 위와
같이 선이 그려지지는 않나요?
저희가 2개까지 인식 되는Window 7 노트북에서 2개는 위와 같이 그려지
던데, 더많은 touch는 Window 7 에서 기본적으로 그려지지 않는 건가
궁금하네요? ( 4~5개 되는 hardware 를 아직 보지 못해서요)
안녕하세요. 포스팅에 관심 가져 주셔서 감사합니다 ^^
관련 업무에 종사하시는 분이시라면 저보다 더 많이 공부하셨을지도 모르겠네요. 저는 관련된 일을 하고 있지는 않아서.. 하하.
일단 이번 포스팅에 올린 예제 프로그램의 스크린샷은 한 번만에 드로잉한 것은 아니고요. 저도 2개씩 여러번 그린 상태의 스크린 샷입니다.
하드웨어에서 동시 인식 가능한 터치의 개수가 여러 개라면, 그만큼의 멀티 드로잉 처리가 가능합니다. 터치 5개가 동시 인식 가능한 장치가 있다면, 한번에 다섯 손가락으로 드로잉 가능한 프로그램을 제작하실 수 있는 거지요.
이번에 소개해 드린 샘플 프로그램도 하드웨어 인식 가능 개수만큼 여러개의 드로잉을 처리할 수 있습니다.
위의 소스코드에서 보듯이 TOUCHINPUT 구조체의 dwID 변수를 통해 각각 분리된 ID의 터치 데이터가 전달되기 때문입니다.
Windows7의 기본 그림판도 제가 구동해 보지는 못했지만, 아마도 하드웨어 지원 한계 범위만큼 터치입력을 모두 처리하게 되어있을 것 같네요 ㅎㅎ
충분히 답변이 되셨는지 모르겠습니다.
답변이 조금 늦었네요. 양해 부탁 드립니다 ^^;
이전 글까지 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 으로 사용할 수 있습니다.
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 을 추상화한 인터페이스입니다. 곧 설명하도록 하겠습니다.
accept() 는 target 에서 호출되지만, source 가 제공합니다. source 의 message 를 수락하고, 소유권이 이전됩니다.
매개변수인 runtime_object_identity 는 message 객체의 msg_id() 로 얻을 수 있습니다. 실제로 runtime_object_identity 는 __int32 를 typedef 한 것이고, Concurrency Runtime 에서 객체를 생성할 때 지정되는 고유의 번호입니다.
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 로 정의되어 있습니다.
지난번 포스팅까지는 멀티터치를 좀 더 손쉽게 구현할 수 있도록 해주는 제스처(GESTURE)를 이용한 방법을 알아보았습니다. 기본적으로는 OS 자체에서 이미 한 번 가공하고 난 데이터를 WM_GESTURE라는 메세지를 전달해 줍니다. 이런 방식을 통해 프로그래머는 가공된 데이터를 바로 사용하기만 하면 된다는 장점도 가지지만, 좀 더 커스텀한 나만의 터치입력 제어처리를 하기에는 다소 제한적이라는 단점도 동시에 얻게 됩니다. 오늘은 이런 경우를 위해, OS의 처리를 아무 것도 거치치 않은 순수 터치입력 데이터인 WM_TOUCH메세지를 이용해 멀티터치 UX를 구현하는 방법을 알아보도록 하겠습니다.
그럼 일단 예제를 살펴보기 전에, WM_GESTOURE로 구현할 수 없는 멀티터치 UX의 예제란 어떤 게 있을지 한 번 이야기 해 보겠습니다. 위에 있는 이미지는 Windows7에 기본으로 포함되어있는 그림판 입니다. 제 PC에서 마우스 두 개를 꽂고, 가상 멀티터치 드라이브를 통해 마우스의 입력을 터치 입력으로 인식하게 변경한 다음, 그림판의 view 영역에 동시에 두 개의 브러시를 통해 그림을 그리고 있는 모습입니다. 멀티터치… 멀티터치니까 당연히 두 개의 panning 제스처 인식이 동시에 처리되는 게 맞겠죠. 제가 마우스가 하나 더 있다면 세 개도 동시에 드로잉 할 수 있을 겁니다. (손은 두 개지만… ㅡ,.ㅡ;..)
‘좋아, 나는 팀 블로그에서 제스처를 이용한 멀티터치 프로그래밍 방법을 익혔으니까 이걸 내가 직접 한 번 짜봐야지!’ 하고 마음을 먹어봅니다. 하지만… 어떻게 구현해야 할 지 막상 감이 잡히질 않는군요. panning 제스처니까 CWnd::OnGesturePan(CPoint ptFrom, CPoint ptTo) 함수를 상속받아서 구현하면 될까요? 근데 입력이 동시에 두 개가 들어오면 함수가 어떤 식으로 호출될까요?
제스처를 통한 구현방법을 사용하는 경우엔 먼저 입력된 한 개의 panning 제스처만 인식이 됩니다. 만약에 제스처를 통해 위에 올린 스크린샷과 같은 저런 터치를 입력하면, OS는 저 것을 두 개의 panning이 아닌 rotate 내지는 zoom 제스처로 번역하게 되겠지요. 그림판에 저렇게 두 개의 라인이 드로잉 되는 모습을 보고 있어서 그렇지, 실제로 저 입력 동작을 지난 번 예제인 사각박스 움직이기 샘플에 입력했다면 당연히 zoom 내지는 rotate의 효과를 기대하게 될 겁니다.
바로 이런 경우, WM_GESTURE가 아닌 WM_TOUCH 메세지를 사용하면 되겠습니다. 동시에 입력되는 두 개의 터치 데이터를 그대로 전달받아서 각각의 panning 제스처로 인식하도록 직접 처리하면 되겠지요. 그렇게 하려면 어떻게 해야 하는지 지금부터 함께 예제를 작성하면서 알아 보도록 하겠습니다.
Task 1 : MFC Application 프로젝트를 만들자.
WM_GESTURE 활용 예제와 유사하게 이번에도 프로젝트 생성부터 단계적으로 알아보도록 하겠습니다. MFC 응용 프로그램 마법사에서 MFC 표준 스타일의 SDI 타입을 지정해 줍니다. wizard의 주요한 설정 화면을 이번에도 스크린샷으로 대신하겠습니다. 지난번과 달리 이번엔 한글판 스크린샷을 찍었네요 ㅎㅎ
BYTE digitizerStatus = (BYTE) GetSystemMetrics(SM_DIGITIZER);
if ((digitizerStatus & (NID_READY | NID_MULTI_INPUT)) == 0)
{
AfxMessageBox(L"현재 터치 입력이 불가능한 상태입니다.");
return FALSE;
}
BYTE nInputs = (BYTE) GetSystemMetrics(SM_MAXIMUMTOUCHES);
CString str;
str.Format(L"현재 %d개의 터치를 동시 인식할 수 있습니다.", nInputs);
AfxMessageBox(str);
위에 있는 코드를 CTouchPadApp::InitInstance() 에 넣고 프로그램을 실행시켰을 때, 아래와 같은 안내 메세지가 나오는걸 확인해 주세요.
하드웨어가 터치를 인식할 수 있는 장치인지를 확인하고 난 다음엔 예제 프로그램의 view 영역이 WM_TOUCH 메세지를 받을 수 있게끔 등록해 주는 절차가 필요합니다. 그렇지 않으면 터치 입력이 들어왔을 때 예전과 똑같이 WM_GESTURE만 날아오게 될 테니까요. 터치 메시지를 받는 윈도우로 등록해 주기 위해서는 CWnd::RegisterTouchWIndow() 함수를 호출해 주면 됩니다. 이 처리를 CChildView의 OnCreate() 함수에서 해주기로 합시다.
Ctrl + Shift + X 키를 눌러서 MFC Class Wizard 창을 띄워줍니다. WM_CREATE 메세지를 처리하는 핸들러를 추가해주세요. 그리고 핸들러에 아래의 간단한 코드를 추가하면 끝입니다 :)
if (!RegisterTouchWindow())
{
ASSERT(FALSE);
}
CWnd::RegisterTouchWIndow() 함수를 호출해주면 윈도우를 터치 윈도우로 등록하게 됩니다. 함수의 인자가 default value 때문에 생략되었는데, 터치 윈도우 등록을 해제하고 싶은 경우도 RegisterTouchWIndow( FALSE ) 를 호출해서 처리합니다. 터치 윈도우로 등록되고 나면 더이상 WM_GESTURE 메세지는 발생되지 않으며, 아무 가공도 거치지 않은 저레벨의 순수 터치 입력 데이터를 전달해 주는 메세지인 WM_TOUCH가 발생하게 됩니다.
헌데 우리는 지금 Win32 프로젝트가 아니라 MFC 프로젝트를 보고 있지요 ㅎㅎ WM_TOUCH를 메시지 프로시저 함수에서 바로 얻어오자는 게 아닌 이상, WM_TOUCH 발생시 호출되는 함수를 알아봐야겠습니다. 우리는 CWnd::OnTouchInput() 함수를 이용하면 되겠군요. 하지만 이 함수는 MFC 클래스 마법사에서는 표시되지 않으니, 직접 손으로 선언과 구현부를 적어주어야 합니다. ChildView.h 파일과 ChildView.cpp에 각각 아래의 코드를 넣어주세요.
// ChildView.h 파일에 추가.
// Overrides
protected:
virtual BOOL OnTouchInput(CPoint pt, int nInputNumber, int nInputsCount, PTOUCHINPUT pInput);
// ChildView.cpp 파일에 추가.
BOOL CChildView::OnTouchInput(CPoint pt, int nInputNumber, int nInputsCount, PTOUCHINPUT pInput)
{
// TODO: Handle Tocuh input messages here
return FALSE;
}
자, 이제 low-level의 터치 데이터를 직접 받아 처리하기 위한 모든 준비가 끝이 났습니다. 이제 사용자가 프로그램의 view 영역에 터치 입력을 할 때마다 OnTouchInput(…) 함수가 호출될 겁니다. 본격적인 터치 데이터의 활용 방법은 다음 포스팅에서 알아보도록 하겠습니다 ㅎㅎ
Outro
이번 포스팅에서는 WM_TOUCH를 이용해 멀티터치 UX를 구현해야 하는 경우에 대한 설명, 윈도우를 터치 윈도우로 등록하는 방법, WM_TOUCH가 발생할 때마다 호출되는 CWnd의 멤버함수 등에 대해 알아보았습니다. WM_TOUCH의 프로그래밍 방법을 알아보기 위한 예제인 TouchPad라는 이름의 프로젝트를 생성부터 기본 설정까지 함께 알아보았고요. 다음 포스팅 에서는 예제를 완성시켜 나가면서, low-level 터치 데이터를 조작하는 코드를 함께 알아보도록 하겠습니다.
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&)> 입니다.
템플릿 매개변수인 _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() 를 사용하는게 바람직합니다.
동기 함수인 receive() 와는 달리 비 동기 함수인 try_receive() 는 수신할 message 가 없을 경우, 기다리지 않고 false 를 반환합니다.
수신할 message 가 있든 없든 바로 반환해야 하는 경우라면 receive() 의 매개변수인 최대 대기 시간을 0 으로 지정하는 것보다는 try_receive() 를 권장합니다.
receive() 의 최대 대기 시간은 예외 메커니즘을 사용하므로 try_receive() 에 비해 오버헤드가 있을 수 있습니다.
[ 그림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
#include
#include
#include
#include
#include
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 부터 지원하는 tr1 의 function 과 visual studio 2010 부터 지원하는 C++0x 의 람다를 사용하였습니다. Concurrency Runtime 은 tr1, C++0x 등의 visual studio 2010 의 새로운 feature 들을 사용하여 구현되었기 때문에 이것들에 대해 알아두는 것이 좋습니다.
[ 그림5. 필터를 이용한 숫자 고르기 예제 실행 결과 ]
마치는 글
이 글에서는 message 전달 함수 중 수신 함수인 receive() 와 try_receive() 에 대해서 알아보았습니다.
receive() 와 try_receive() 는 사용해야 할 상황이 분명히 다르니 상황에 따라 사용에 유의해야 합니다.
다음 글에서는 message 가 저장되는 message block 에 대해서 알아보도록 하겠습니다.
지금까지는 닷넷 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;
//현재 작업중인 스레드에서도 반복문 시작 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;
//혹시 현재 스레드가 빨리 끝나더라도, //추가 스레드가 끝날 때 까지 기다리기. 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
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 가 전송에 대한 결과가 확실해 졌을 때 반환됩니다. 즉, 전송된 결과가 확실할 때까지 기다린다는 뜻입니다.
#include
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 전송은 거부됩니다.
이 예제에는 message block 의 필터로 Visual studio 2010 에서 지원하는 C++0x 의 람다를 사용하였습니다. 람다는 이 글의 논제에서 벗어나기 때문에 설명하지 않도록 하겠습니다. Visual studio 팀 블로그에서 람다에 대한 정보를 얻을 수 있습니다.
간단히 람다에 대해서 설명하고 넘어가자면 익명의 함수 객체라고 보셔도 될 것입니다.
예제에 사용된 message block 의 필터는 짝수만 수락하는 필터입니다. 그래서 실행 결과로 send() 와 asend() 모두 홀수는 거부되었고, 짝수는 수락되는 것을 볼 수 있습니다.
[ 그림4. 전송 거부 예제 실행 결과 ]
마치는 글
이 글에서는 message 전달 함수 중 전송 함수들에 대해서 알아보았습니다.
이 함수들 중 어떤 것을 사용하는 것이 적절한지를 판단하기 위해서는 반드시 동기와 비 동기에 대한 개념의 이해가 필요합니다.
상황에 따라 적절한 함수를 사용하시면 원하는 결과를 얻을 수 있을 것입니다.
전송 함수들에 대해서 알아보았지만 아직 수신 함수들에 대해 알아보지 않았습니다. 다음 글에서는 message block 으로부터 message 를 수신하는 수신 함수들에 대해 알아보겠습니다.
C/C++에서 포인터를 초기화 할 때 ‘NULL’을 사용합니다. 그러나 VC++ 10에는 C++0x에서는 포인터를 초기화 할 때 NULL 대신 새로 생긴 ‘nullptr’을 사용할 수 있게 되었습니다.
C++/CLI는 이전부터
nullptr이 있었습니다.
C++/CLI에서는 ref 클래스의
핸들을 초기화 할 때는 nullptr을 사용합니다.
C++/CLI, C++0x의
nullptr은 C/C++ 처럼 ‘0’이 아니라는
것을 잘 기억하시기 바랍니다.
interior_ptr
interior_ptr은 관리 힙(managed
heap. 즉 GC겠죠) 상의 value type나 기본형을 가리키는 포인터라고 할 수 있습니다.
interior_ptr는 value type나 기본형을 비관리 코드의 포인터처럼 사용하고
싶을 때 사용하면 좋습니다.
< 코드 1. >
ref class REFClass
{
public:
int nValue;
};
void SetValue( int* nValue )
{
*nValue = 100;
}
int main()
{
REFClass^ refClass = gcnew REFClass;
SetValue( &refClass->nValue ); // 에러
}
위 코드를 빌드 해 보면 SetValue(
&refClass->nValue ); 에서 빌드 에러가 발생합니다. 매니지드
힙에 있는 것은 그 위치가 변하므로 비 관리 코드의 포인터를 넘길 수가 없습니다. 그럼 <코드 1>를 정상적으로 빌드 하기 위해서 interior_ptr를 사용해 보겠습니다.
< 코드 2. >
ref class REFClass
{
public:
int nValue;
};
void SetValue( interior_ptr<int>
nValue )
{
*nValue = 100;
}
int main()
{
REFClass^ refClass = gcnew REFClass;
SetValue( &refClass->nValue );
}
<코드 2>의 SetValue의 파라미터로 비관리 코드의 참조나 포인터를 넘길 수도 있습니다.
< 코드 3. >
#include <iostream>
void SetValue( interior_ptr<int>
nValue )
{
*nValue = 100;
}
int main()
{
int
nValue = 50;
SetValue(
&nValue );
std::cout
<< nValue << std::endl;
getchar();
return
0;
}
그리고 interior_ptr에 대신 C++/CLI의 참조(‘%’)를 사용하는 방법도 있습니다.
pin_ptr
pin_ptr은 관리 힙 상의
value type나 기본형을 비관리 코드에서 포인터로 사용하고 싶을 때 사용하는 기능입니다. 가장
필요한 경우가 C++/CLI에서 기존의 비관리 코드로 만들어 놓은 라이브러리를 사용할 때입니다.
< 코드 4. >
ref class REFClass
{
public:
int nValue;
};
void SetValue( int* pValue )
{
*pValue = 100;
}
int main()
{
REFClass^ refClass = gcnew REFClass;
pin_ptr<int> pValue = &refClass->nValue;
SetValue( pValue );
pValue = nullptr;
}
pin_ptr에 메모리 주소를 할당하는 것을 ‘pin’이라고 부르고 사용이 끝난 후 nullptr로 초기화 하는
것을 ‘unpin’ 이라고 부릅니다. pin_ptr 사용이
끝난 후 가능한 빨리 unpin 해주는 것이 좋습니다.
interior_ptr과 pin_ptr의 차이점
interipor_ptr과
pin_ptr은 둘 다 관리 힙 상의 value type이나 기본형을 가리키는 포인터로
사용되지만 interior_ptr은 관리 힙 상에서 인스턴스가 이동하여도 올바르게 추적할 수 있는 포인터로
런타임의 지배하에 있습니다(즉 인스턴스가 관리 힙 상에서 이동하여도 괜찮습니다).
pint_ptr은 관리 힙 상의
value type을 비관리 코드에서 사용하고 싶을 때 사용합니다. 당연히 이 때는 관리
힙에 있는 인스턴스가 이동하면 안되므로 인스턴스의 이동을 금지합니다.
interipor_ptr과
pin_ptr의 같은 점 : 포인터처럼 사용할 수 있다.
interipor_ptr과
pin_ptr 다른 점 : interipor_ptr은 관리 코드 상에서 포인터로 사용하고, pin_ptr는 비관리 코드에 포인터로 넘길 때 사용합니다.
interipor_ptr과
pin_ptr을 공부했으니 다음에는 C++/CLI에서 비관리 C++과 혼합해서 사용할 때 어떻게 해야 하는지 설명하겠습니다.
저는 늘 의문을 품어왔습니다...는 훼이크고 이번 포스트를 준비하면서 의문을 가지게 되었습니다. 분명 병렬 프로그래밍의 정신은 남아도는 코어를 활용해서 협력을 해서 작업을 좀 더 빨리 끝내자고 하는 건데요, 그런면에서 '백지장도 맞들면 낫다'는 말은 병렬 프로그래밍의 정신을 잘 표현하는 선조들의 지혜라고 볼 수 있습니다. 그런데요.... 과연 백지장같이 갓난 아기도 혼자들 수 있는 걸 같이 드는게 과연 나은 일일까요? 오히려 혼자 할 때보다 못한 결과를 가져오지는 않을까요? 오늘은 그에 대한 이야기입니다.
- 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();
<코드1>을 보시면, 1부터 1000까지의 숫자를 가진 배열을 생성하고, 각 수를 제곱한 수를 구하는 코드입니다. 기존의 LINQ코드와 다른 점이 있다면, 제곱 연산을 수행하기 위한 데이터 소스인 nums에 대해서 AsParallel()을 호출했다는 것입니다. <코드1>에선 AsParallel()의 리턴타입이 ParallelQuery<int>인데요, LINQ에서는 Enumerable을 사용하지만, PLINQ에서는 ParallelEnumerable을 사용합니다.
<코드1>을 보면, 정말 간단하게 병렬 프로그래밍이 구현되는데요. 정말 저렇게 간단한 방법으로 병렬 쿼리가 실행되는지 확인하기 위해서 Task.CurrentId를 통해서 실행중인 스레드의 Id를 출력하도록 했습니다. 그리고 비교적 일관성 있는 결과를 얻기 위해서 Thread.Sleep를 통해서 실행을 조금 여유롭게 해줬죠. 결과를 보실까요?
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->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();
<코드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
프로그래밍 할 때 가장 자주 사용하는 자료구조가 바로 배열입니다. 배열을
사용하지 않고 프로그래밍 하기는 힘들죠^^.
그래서 이번에는 C++/CLI에서의 배열에 대해서 이야기하려고 합니다.
C++/CLI에서의 배열은 ‘array’
비관리 C++에서는 배열은 ‘[]’을
사용합니다.
int Nums[10];
char szName[20] = {0,};
그러나 C++/CLI에서의 배열은 ‘array’라는
클래스를 사용합니다.
int 형의 3개의 요소를
가지는 배열은 아래와 같이 정의합니다.
array< int >^ A1 = gcnew array<
int >(3);
array< int >^ A2 = gcnew array<
int >(4) { 1, 2, 3 };
array< int >^ A3 = gcnew array<
int >{ 1, 2, 3 };
다음은 간단한 사용 예입니다.
< 코드 1. >
int main()
{
array<
int >^ Numbers = gcnew array< int >(5);
for(
int i = 0; i < 5; ++i )
{
Numbers[
i ] = i;
System::Console::WriteLine(
Numbers[i] );
}
getchar();
return
0;
}
array에 유저 정의형 사용하기
array에는 기본형(int,
float 등)만이 아닌 유저 정의형도 사용할 수 있습니다. 다만 비관리 클래스는 안됩니다. 오직 관리 클래스(ref class)만 가능합니다. 또 그냥 ref 클래스를 그대로 넣을 수는 없는 클래스의 핸들을 사용해야 합니다(ref
클래스는 GC에 동적 할당을 하기 때문이겠죠).
ref class refTest
{
};
array< refTest >^ arrTest;// 에러
array< refTest^ >^ arrTest;// OK
for each 사용하기
앞서 <코드1>의
예제에서는 배열의 모든 요소를 순환하기 하기 위해 ‘for’문을 사용했습니다. 그러나 .NET에서는 for문
보다 ‘for each’문을 사용하는 것이 성능이나 안정성 등에서 더 좋습니다(다만 for each를 사용하면 내부에서 값을 변경할 수 없다는 문제는
있습니다).
< 코드 2. >
#include <iostream>
int main()
{
array<
int >^ Numbers = gcnew array< int > { 10, 11, 12, 13, 14 };
약속은 하는 것 보다, 취소하는 게 어렵습니다. 게다가 취소할 때는 적절한 타이밍을 놓치면, 안 좋은 기억만 남기게 되죠. 그래서 프로그램에서도 취소를 제대로 할 수 있도록 지원하는 게 중요합니다. 누구나 실수 할 수 있거든요.
- 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;
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....") );
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만쉐이! 어헣.
이번 포스팅에서는 앞 글에서 이야기 중이었던 예제 프로그램의 나머지 부분을 마저 작성하고, MFC 기반의 프로젝트에서 제스처를 이용해 멀티터치 프로그래밍을 제어하는 방법에 대한 보충 설명을 정리해 보도록 하겠습니다. 앞에 적은 글과 이어지는 내용이니까, 앞부분을 보지 못하신 분들은 ( http://vsts2010.net/292 ) 이 글을 먼저 읽어주세요 ^^ 지난번 글에서는 멀티터치 동작을 확인하기 위한 Drawing 처리까지 함께 알아봤습니다. 앞부분에서 진행됐던 내용을 소제목만 간추려서 알아보면,
* 제스처를 인식하는 첫 번째 예제 프로그램 *
MFC Application 프로젝트를 만들자
하드웨어 상태를 알아봅시다
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);
기본적으로 터치가 인식되면 애플리케이션으로 WM_GESTURE 메세지가 날아옵니다. Win32 API만을 이용해 제스처를 제어하려면 WndProc 프로시저 함수에서 WM_GESTURE를 잡아서 처리해야 하지만, MFC 애플리케이션의 경우 자체적으로 각각의 제스처에 대한 전용 핸들링 함수들이 나뉘어져 있기 때문에, 제어가 필요한 제스처에 해당하는 함수를 용도에 맞게 따로따로 오버라이딩 해서 사용하면 됩니다.
Note : 그런데 CWnd의 선언을 살펴보면 (afxwin.h 파일에 있습니다.) 위에 나열된 함수 말고도 OnGesture 함수가 정의되어 있는 것을 확인하실 수 있습니다. 함수의 원형은 아래와 같습니다.
오호라~ 이것은 WM_GESTURE 메세지가 넘어올 때마다 해당 메세지를 직접 제어할 수 있는 함수인가 보군요~ 함수의 인자로 wParam, lParam이 모두 날아오니 win32 프로젝트에서 하는 것처럼 코딩하고 싶을 땐 이걸 상속받아서 작업하면 되겠구나 ~ … 라고 생각하고, 실제로 테스트 코드도 만들어 봤었지만 안되더군요 @.@… 똑똑한 여러분들은 이유가 무엇인지 바로 찾으셨을 거라고 생각합니다만… 저 함수는 가상 함수가 아닙니다 ^^;… 그저 이름만 보고 상속받아 써야지 했는데, 나중에 보니 재정의 할 수 없는 일반 멤버함수더라고요 ㅎㅎ 아마도 CWnd가 자체적으로 처리하는 코드를 구현한 부분이 아닌가 생각됩니다. 일반적으로는 각각의 제스처마다 독립적으로 호출되는 위의 다섯 가지 함수를 이용하면 되고요, 경우에 따라 부득이하게 WM_GESTURE 메세지를 직접 제어하고 싶을 때엔… 이전에 그랬던 것처럼 WndProc을 직접 제어하도록 하고 그곳에서 WM_GESTURE를 받은 경우에 대한 switch-case 문을 넣어주면 되겠죠 ^^
이제 위에 소개된 다섯 개의 함수들에서 view 영역에 그려지고 있는 사각형을 제어하는 코드들을 넣어줍니다. 아래에 함수들의 본문 코드가 있습니다.
함수의 이름과 전달되는 인자들 모두 직관적입니다. 함수 본문의 예제 코드들도 그리 어렵지 않군요. 위의 코드들을 복사해 적어준 뒤 빌드하고 프로그램을 실행해 봅니다. 애플리케이션이 뜨면 사각형을 손으로 움직여 보세요. 터치 입력에 따라 사각형은 이동하고, 늘어나고, 회전할겁니다 :)
네…? 근데 뭔가가 안 된다고요?
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를 이용한 멀티터치 프로그래밍 방법을 알아 보도록 하겠습니다. 혹시 그 전에 질문 사항들이나, 제스처 프로그래밍 방법들에 대한 추가 학습사항이 있다면 다시 한 번 정리하는 기회를 갖도록 하겠습니다.
그럼 다음 포스팅에서 다시 인사 드리도록 하겠습니다. 그 때까지 더운 날씨에 모두들 건강하시고 공부 열심히 하세요 ~ ^^*
댓글을 달아 주세요