'Programming/C/C++ Programming'에 해당되는 글 3건

  1. 2009.08.17 C++ 캐스팅 연산자 소개 (1) - static_cast 2
  2. 2008.08.24 데이터를 자동으로 정렬해서 저장한다 - CSortedArray, CSortedArrayEx
  3. 2008.08.24 멀티스레드 입출력에 최적화된 버퍼 템플릿 - CSwappableCollector

C++ 캐스팅 연산자 소개 (1) - static_cast

Programming/C/C++ Programming
2009. 8. 17. 19:18, Posted by ScottRhee

캐스팅은 기존 C에서도 지원하는 기능이며, 형변환을 할 때 마구잡이로 사용하던 기술이기도 했다. 상식적으로 불가능한것만 아니면 웬만한 변환은 다 지원해버렸기 때문에 마구마구 썼었지.. 별도의 키워드조차 필요치 않고 타입에다 괄호만 사용해주면 되니 사용법도 무지 간단하다.  C++을 배우고 나서도 C++을 위한 별도의 캐스팅 연산자가 있는줄도 몰랐으니 말 다했지 뭐. 

그런데 이건 어떨까. 

class CClass1
{
public:
int m_intTest;
};

class CClass2
{
public:
int m_intTestTmp;
int m_intAdditional;
};


void main()
{
CClass1 cl1;
CClass2* pcl2 = (CClass2*)&cl1;

pcl2->m_intAdditional = 0;
}

이 코드의 경우 컴파일타임에는 전혀 에러가 나지 않는것이 당연하다. C의 캐스팅은 적당한 캐스팅연산자만 써주면 그냥 만사 다 OK거든.. 심지어는 char szTest = (char)"어버버"; 이런거나 int *pintTest = (int*)'a'; 이런 괴상한 코드도 경고 없이 다 OK해버린다. 

암튼 위 코드를 실행하면, 그 결과는 당연히 크래시..... CClass2의 인스턴스가 갖게 될 메모리 사이즈가 CClass1보다 더 클테고, 따라서 CClass2의 포인터로 CClass1의 인스턴스를 액세스하게 되면 메모리크기의 차이때문에 fault가 생기게 되는 것이다. 이것이 C캐스팅의 한계라고 할 수 있다. 물론 미리 코드를 잘 검사해서 저런 코딩을 할 확률을 낮춰줄 수 있으면 좋은데, 그게 어디 쉬운가.. 슬픈 것은 상당수의 C++소스가 이런식으로 되어 있어서 디버깅이 무지 어렵다는 것이다. 물론 나도 단지 귀차니즘을 이유로 저런식의 코딩을 참 많이 했는데.. 일단 지금부터라도 가급적 C++전용의 캐스팅 연산자만 써보려고 한다. 코딩할 때 약간 귀찮은 것이, 디버깅하느라 밤새는 것보다는 훨씬 이익이니까. 

그럼, 포인터끼리 캐스팅을 할 때 가장 문제가 되는 상황이 어떤 것일까? 내 생각에는 대충 두 가지인 것 같다. 

첫째, 아예 다른 클래스끼리의 캐스팅.
둘째, 상속/피상속 관계에 있는 클래스에서, 자식클래스의 포인터가 부모클래스의 인스턴스를 가리키도록 하는 캐스팅. (또는 그 역)

첫째는 웬만하면 그냥 무조건 막으면 되니까 이해하기 쉬우나 둘째의 경우는 좀 더 세밀한 판단(상속/피상속 관계 확인)이 요구됨을 예상할 수 있을 것이다. 

아무튼 첫번째 상황을 체크하기 위해서 main 코드 부분을 다음과 같이 바꿔봤다. 

CClass1 cl1;
CClass2* pcl2 = static_cast<CClass2 *>(&cl1);

pcl2->m_intAdditional = 0;

아까와는 달리, 컴파일 타임에 에러가 발생한다. 

error C2440: 'static_cast' : cannot convert from 'CClass1 *__w64 ' to 'CClass2 *'
        Types pointed to are unrelated; conversion requires reinterpret_cast, C-style cast or function-style cast

대충 보면, 두 클래스간 관계가 없어서 캐스팅에 실패했다고 한다. C스타일 캐스팅을 사용하든지, reinterpret_cast를 사용하라고 되어 있다. C스타일 캐스트는 말 그대로 C의 캐스팅이니 앞서 본 소스처럼 코딩을 하라는 것이고, reinterpret_cast는 C++에 존재하는 기존 C식 캐스팅이라고 생각하면 될것 같다. (다만 이녀석은 기본타입간 변환은 불가하고, 포인터와 관계된 변환만 제공하므로 분명한 차이가 있기는 하다.) 함수식 캐스팅은.. 음 기회가 되면 다음에.. 

단지 캐스팅 연산자만 바꿨을 뿐인데도, 전혀 다른 클래스를 실수로 캐스팅에 우겨넣는 실수는 하지 않게 되었다. Olleh!

여기까지는 이해하기가 쉬운데, 만약 두 클래스가 부모/자식 관계라면 어떨까? (위에서 살펴 본 두 번째 상황에 해당) 어쨌든 서로 다른 클래스이므로 캐스팅을 거부할 수도 있고, 연관성이 있으니 캐스팅을 허용할 수도 있을 것이다. 백문이 불여일견! 고고싱 

class CParent
{
public:
int m_intTest;
};

class CChild : public CParent
{
public:
int m_intTest2;
int m_intAdditional;
};

void main()
{
CParent clParent;
CChild clChild;

CParent *pclParent; 
CChild *pclChild;

pclParent = &clChild; // OK
pclChild = &clParent; // ERROR
pclChild = static_cast<CChild*>(&clParent); // OK
}

1. 부모클래스의 포인터에서 자식클래스의 인스턴스를 받을 때 : 캐스팅 안해도 OK (당연하지.. 소멸자 문제만 빼면)
2. 자식클래스의 포인터에서 부모클래스의 인스턴스를 받을 때 : 캐스팅 안하면 에러 (역시 당연함.. 자식클래스 고유의 멤버가 사용될 우려가 있음) 
3. 자식클래스의 포인터에서 부모클래스의 인스턴스를 받으면서 static_cast를 사용한경우 : OK 

즉, static_cast의 경우는 자식클래스 포인터에서 부모의 인스턴스를 받아도 경고없이 그냥 허용한다는거다. 아예 다른 타입의 경우 변환을 불허하지만, 상속관계에 있는 클래스의 경우에는 C스타일의 캐스트처럼 그냥 다 허용한다는 것. 결국 부모클래스의 인스턴스로 차일드 클래스의 멤버를 호출하면 문제가 발생하게 되는 것이다. 

그럼 static_cast를 아예 안쓰면 안전하지 않냐고? 인스턴스 관리에 있어서 항상 최하위 클래스의 포인터로만 작업하기 어렵다는 것은 경험상 알고 있을 것이다. (사과, 복숭아, 오이, 마늘 등을 모아놓고, 한큐에 "껍질까기"메서드를 호출하고 싶을 때처럼..) 

과거에 상속이 얽히고 설킨 상황에서 포인터로 멤버 함수를 호출하고자 했을 때 함수 선언을 virtual로 하여 런타임에 인스턴스 종류를 파악하게 하여 메서드 호출에서의 폴트를 피했던 것처럼, 캐스팅과 관련해서도 유사한 방법이 필요하다. 캐스팅 연산자에서는 dynamic_cast가 그런 역할을 하고 있어서, 상속관계에 있는 클래스의 포인터간에 안전한 타입변환을 해주고, 문제의 소지가 있는 형변환에 대해서는 null을 전달하여 오동작을 방지하게 해준다. dynamic_cast에 대해서는 다음 시간에..

아 그럼 여기서 또 질문이 나올 수 있는데. dynamic_cast로 안전하게 상속 클래스간 포인터변환을 할 수 있다면, static_cast가 굳이 상속관계 클래스 포인터간 캐스팅을 지원하는 이유가 뭐냐? 그냥 지원 안해버리면 되는데.... 맞는 말이다. 

이것은 virtual 함수의 특성과 비슷한 답변을 하면 될 것 같다. virtual 함수를 사용하게 되면, 실시간 타입검사(RTTI)가 들어가기 때문에 아무래도 속도상 손해가 있다. 결국 퍼포먼스가 무지막지하게 중요한 소프트웨어라서 RTTI를 빼버리고 컴파일하고 싶거나, 컴파일러가 특이해서 RTTI옵션을 아예 지원하지 않는다면 virtual 함수를 쓸 수 없듯이, dynamic_cast역시 RTTI를 쓸 수 없는 환경에서는 사용할 수가 없다. 이것은 dynamic_cast를 소개할 때 한 번 더 언급하게 될 것 같다. 아무튼 이런 상황에서 만능열쇠와도 같은 (=위험하기 짝이 없는) C스타일 캐스팅보다는 그나마 static_cast를 사용해주는게 차선책이 될 수 있음을 이해할 수 있을 것이다. 이쯤되면 왜 한놈은 스태틱이고 한놈은 다이내믹이라는 이름을 갖고 있는지도 충분히 이해가 될 듯. 

한 가지 재미있는 것은, static_cast의 경우 기본적으로 상속관계가 아닌 클래스끼리는 포인터변환을 할 수 없지만, 중간에 void* 로 한 번 변환을 해두면 자유롭게 캐스팅이 가능하다는 것. 

class CClass1
{
public:
int m_intTest;
};

class CClass2
{
public:
int m_intTest2;
int m_intAdditional;
};

void main()
{
CClass1 *pcl1; 
CClass2 *pcl2;
void *ptemp;

pcl1 = static_cast<CClass1*>(pcl2); // ERROR

ptemp = static_cast<void*>(pcl2); // OK
pcl1 = static_cast<CClass1*>(ptemp); // OK
}

나의 미천한 실력으로 정확한 이유는 알 수 없으나, 모든 클래스를 void를 상속한 걸로 간주하기 때문에 그런것이 아닐까 추측한다. 아니면, 파일 처리나 통신 등에서 serialize를 지원하게 하기 위해서일지도 모르고.. 또는, malloc등의 함수를 통해 고정된 타입으로 메모리주소를 넘겨받는 경우가 있어서 그런지도 모르겠다.  

암튼 이렇게까지 할거면 걍 C캐스팅 쓰는거나 무슨 차이냐 싶을 수도 있지만, 명시적으로 강제 캐스팅이 불가피하다는 것을 보여주기 위해선 나쁘지 않을것 같기도 하다. 

사실 여기서 소개한 것 외에도 몇 가지 특성이 더 있기는 한데.. 나중에 생각나면 추가해 보도록 하겠다.
다음 시간에는 dynamic_cast에 대해서 살펴볼 것이다. 

그리고 끝으로 언급해야 될 것이 한 가지 있다면.. C++에는 오늘 살펴본 static_cast처럼 C++특유의 네 가지 정도의 캐스팅 연산자가 있으며, 앞서 대강 살펴봤듯이 각각의 역할이 뚜렷이 구분되어 있다. 이것은 프로그래머에게 실수를 방지하도록 돕는 것과 동시에, 타인이 소스를 읽어봤을 때 프로그래머의 의중을 쉽게 파악할 수 있도록 하는 역할도 겸한다. 그런데 이런 상황에서 갑자기 중간에 C스타일의 캐스팅이 툭 튀어나온다면? (워3 용어로, 갑툭튀라고 할 수 있겠다) 그동안 쌓아놓은 무결성과 가독성이 와르르 무너지게 되는 것이다. 일반 타입 변환에 static_cast를 쓰라고까지는 못하겠지만(그러나 실제로 이렇게 프로그래밍하는 사람도 많음), 클래스 또는 포인터 변환에서만이라도 C++ 캐스팅 키워드를 반드시 사용해보면 어떨까 싶다. 그리고 이 캐스팅 기법들은 포인터뿐만 아니고 레퍼런스에도 동일한 방식으로 지원을 하고 있으니 레퍼런스 변환에도 적극적으로 사용해보도록 하자. 


:

데이터를 자동으로 정렬해서 저장한다 - CSortedArray, CSortedArrayEx

Programming/C/C++ Programming
2008. 8. 24. 23:01, Posted by ScottRhee

특정 타입의 데이터를 저장하긴 해야겠는데, 이것을 자동으로 정렬해가면서 저장하고 싶을 때가 있습니다. 이럴 때 사용하는 자료구조입니다. 템플릿 형태로 되어 있기 때문에 타입만 지정해주면 됩니다.


이 템플릿 클래스의 원래 출처는 이곳입니다 : http://www.naughter.com/sortedarray.html


남의 소스를 왜 자기것처럼 업로드했냐고요? 그것은, 이 소스의 최신버전은 VS2005 이상만 지원을 하기 때문입니다. 이 포스트에 올라온 버전은, VS2003에서도 쓸 수 있도록 약간 수정을 가한 버전입니다. (디버그 관련 명령어 하나만 바꾼 거지만.-_-) 첨부파일을 참고하세요. 그 부분 이외에 다른 Copyright부분이라든지 코멘트 부분 등은 전혀 손을 대지 않았습니다.  


내부 구조는, MFC를 이용하는 프로젝트일 경우 CArray템플릿을 이용하며, 그렇지 않을 경우 CSimpleArrayEx를 이용하도록 되어 있습니다. (CSimpleArrayEx는 ATL의 CSimpleArray를 확장하여 삽입 기능을 추가한 버전인데, ATL 라이브러리쪽으로 작업을 하시는 분은 분은 위의 출처 링크로 가서 CSimpleArrayEx의 소스를 받아서 추가하시면 ATL버전을 쓸 수 있습니다.) 


CSortedArray는 자체적으로 Binary Tree를 사용하여 정렬과 검색을 빠르게 실시합니다. 입력과 동시에 정렬이 자동으로 되기 때문에, 따로 정렬 알고리즘을 구현하지 않아도 된다는 결정적인 장점이 있습니다. 다만, CArray를 상속한 것이기 때문에 직접 어레이에 값을 쓰는 메서드도 오픈이 되어 있어서, 수동으로 정렬을 하는 메서드도 따로 준비를 해두었네요.


사용 방법은 무척 간단합니다. 메모리 관리 클래스를 사용해보신 경험이 한번이라도 있다면, 사용상 전혀 무리가 없을 만한 수준입니다. 다만, 자료구조에 담기는 데이터가 항상 단순 수치인 것은 아니기 때문에, 서로의 값을 비교하는 룰(비교함수)을 제공해야 합니다.


이 라이브러리에는 버전이 두 개가 있습니다. CSortedArray와 CSortedArrayEx가 그것인데요, 기능적인 부분은 같고, 다만 CSortedArray는 일반적인 콜백 함수로 비교 함수를 지정하며, CSorrtedArrayEx는 콜백함수가 아닌 콜백클래스(Functor)를 이용하는 점이 다릅니다. 펑터에 대해서 궁금하신 분은 http://www.newty.de/fpt/functor.html 링크를 참조하시기 바라며, 간단히 설명드리면 컴파일타임에 메서드가 인라인 처리되어 빠른 속도를 낼 수 있는 처리방법이라고 생각하시면 됩니다. 콜백으로 펑션과 펑터를 사용한다는 차이점을 제외하면, 나머지 기능은 완전히 동일합니다. 다음은 콜백 구현의 예입니다. 왼쪽이 펑션, 오른쪽이 펑터입니다.


사용자 삽입 이미지


왼쪽 함수야 따로 설명드릴건 없을것 같고.. 오른쪽 펑터 버전은 함수 호출에 해당하는 오퍼레이터를 재정의해서 빠른 속도를 꾀하고 있군요. 실제 개발자가 테스트해본 바에 의하면 훨씬 속도가 빠르다 합니다. 펑터의 장점이지요. 사용법이 많이 어려운 것도 아니니 펑터 버전의 사용을 권장합니다. 다만, VS2008에서는 컴파일러 최적화 기능이 좋아져서 거의 차이가 없어진다고는 하는군요. 창피한 일이지만 함수 호출도 오퍼레이터로 처리되는줄은 미처 몰랐습니다^^;;


펑터 버전, 즉 CSortedArrayEx를 가지고 간단한 예제 프로그램을 만들어 보겠습니다. 맨 윗줄에 있는 CompareIntsClass는 물론 앞서 적어둔 비교용 콜백 펑터입니다. 당연하지만 소스에 그것을 먼저 적어주셔야 합니다.

CSortedArrayEx<int, int&, CompareIntsClass> idata;

 int d = 7;  idata.OrderedInsert(d);
 d = 3;  idata.OrderedInsert(d);
 d = 4;  idata.OrderedInsert(d);
 d = 5;  idata.OrderedInsert(d);
 d = 2;  idata.OrderedInsert(d);

템플릿의 파라미터는 각각 저장할 값의 타입, 참조 타입, 그리고 비교펑터로 선언하게 되어 있습니다. 그리고 OrderedInsert가 데이터를 정렬시켜가며 입력하는 메서드입니다. 아주 간단하지요? 이렇게 데이터를 무작위로 넣어도, 어레이에는 항상 0번째 인덱스부터 오름차순으로 데이터가 정렬되어 저장된다는 겁니다. 즉, 아래 루틴을 루프로 돌리면 작은 수부터 출력이 잘 이루어지지요. 2, 3, 4, 5, 7 이렇게 출력된다는 겁니다. RemoveAt(0)으로 값을 빼내도 그즉시 다시 자동정렬이 돼서 옆에 있던 값이 채워집니다.

 int intData = idata.GetAt(0);

 printf("%d\n", intData); idata.RemoveAt(0);

자, 그럼 이것을 어디에 이용하느냐? 저는 이것을 서버-클라이언트 이벤트 처리에 이용했었습니다. "특정 시각에 해야 할 일"을 처리하는 용도로 사용했지요. 시각을 수치화시켜서(GetTickCount등을 이용) 위 자료구조에 집어넣으면, 가장 빠른 시간에 해야 할 일을 항상 찾아낼 수가 있습니다. 현재 시간과 비교해서 맞는 시간에 해당하는 일부터 처리하고, 현재 시각에 맞는 일이 아직 없으면 기다리면 되니까 편리하게 이용할 수가 있는 거지요. 기타, 데이터 입력이 런타임으로 계속 일어나는 그런 상황에서의 정렬에 쓰시면 됩니다. 하지만, 데이터 입출력 시점이 명확하게 정해진 상황에서의 정렬이라면 일반적인 정렬 알고리즘을 쓰는 게 더 좋은 방법이 될 수도 있겠죠.



결론


용도 : 우선순위, 또는 자동정렬이 필요한 작업

장점 : 추가적인 정렬 함수 호출 없이도 항상 정렬된 결과를 보장해준다. Binary Tree를 이용하기에 따로 정렬을 하지 않으므로 퍼포먼스가 뛰어나다. Functor 콜백을 선택적으로 제공하여 상황에 따라 편리하게 사용할 수 있다.

단점 : 중복 값이 있을 경우에는 탐색 기능만으로는 원하는 오브젝트를 바로 찾아낼 수 없다. ATL도 MFC도 이용할 수 없는 환경에서는 쓸 수 없다. (..단점이라 하긴 좀^^)

:

멀티스레드 입출력에 최적화된 버퍼 템플릿 - CSwappableCollector

Programming/C/C++ Programming
2008. 8. 24. 22:56, Posted by ScottRhee

산발적으로 여러 곳에서 발생하는 이벤트나 데이터를 처리할 때에는, 일반적으로 데이터를 순차적으로 쌓아둘 수 있는 자료구조를 하나 운영하면서 프로세스/스레드 동기화 과정을 이용해 입출력을 하게 됩니다.


그런데, 어떤 애플리케이션에서는, 데이터를 입력하는 곳과 출력하는 곳이 완전히 분리되어 있기도 합니다. 예를 들어, 국회의원 선거를 할 때, 유권자들은 투표용지를 투표함에 넣기만 하지, 투표함에서 다시 용지를 빼오지는 않습니다. 반대로, 투표를 집계하는 사람은 표를 빼내기만 하지, 다시 투표함에 표를 넣는 일도 없습니다. 이런 식으로 입출력 방향이 명확한 데이터에 대해서는, 프로세스 동기화를 분리하는 것이 효율적입니다. 다시 말해서, 위의 예에서는 표를 넣는 사람과 빼오는 사람이 동시에 일을 해도 문제가 없다는 것이지요.


아시다시피 프로세스 동기화 작업은 가능한 적을 수록 좋습니다. 동기화 자체의 오버헤드도 문제고, 특히 현재 시스템의 CPU 코어 수에 맞춰서 스레드를 생성시킨 경우, 동기화때문에 스레드가 대기하게 되면 고효율과는 상당히 거리가 멀어집니다.  


하지만, 대부분의 자료구조 클래스는 입력과 출력의 동기화를 구분없이 함께 해주지 않으면 Fault를 일으킵니다. 데이터를 관리하는 내부 구조체나 플래그 등이 이를 구분하지 않기 때문입니다. 이렇게 되니 동기화를 따로 해주고 싶어도 그렇게 할 방법이 존재하지 않는 셈입니다. 결국 데이터를 입력받는 오브젝트와, 데이터를 출력하는 오브젝트를 분리하지 않으면 데이터를 입력하는 동안에 출력모듈까지 대기를 하게 될 가능성이 생기게 되므로 (반대의 경우도 마찬가지) 비효율적이라고 할 수 있습니다.


헌데 입/출력 오브젝트를 분리하게 되면, 동기화 효율은 높아지지만, 입력데이터를 모아서 출력데이터로 전달하는 과정이 추가로 필요해지게 됩니다. 당연한 얘기지만 이 과정에서도 또 동기화가 필요해지게 되고요. 위의 투표상황의 예를 들면, 투/개표 중간에 작업을 올스톱시키고 투표용 투표함에서 표를 모두 꺼내서 개표용 투표함에 쏟아넣는 과정이 필요해지는 셈이죠. 데이터량이 많을 경우, 이 작업때문에 타 작업스레드가 오랜시간 블러킹될 확률이 높아집니다. 결국 이런 식으로 작업을 하면 역시 고효율과는 거리가 멀어지게 됩니다.


그렇다면 이 방법은 어떨까요? 투표용 투표함과 개표용 투표함을 두기는 하되, 일정시간마다 개표용 투표함과 투표용 투표함을 바꿔치기하는 겁니다. 기표된 투표용지를 꺼내서 옮기는게 아니고, 통의 위치만 바꿔치기하는 거지요. 투표함을 바꿔치기할 때에는 물론 다른 작업을 모두 올스톱시켜야 하지만, 바꿔치기만 하는 것은 굉장히 고속으로 할 수가 있거든요. C에는 이런 작업에 쓸 수 있는 유용한 도구가 존재합니다. 포인터지요. 이 작업을 할 수 있는 템플릿을 작성해 보도록 합시다.


우선, 템플릿 선언과 Attributes입니다.

template <class objT>
class CSwappableCollector
{
private:

 objT  m_coll[2];
 objT *m_pcollInput;
 objT *m_pcollOutput;


 CCriticalSection m_csInput;
 CMutex    m_mtOutput;

템플릿 클래스이므로 입력받을 데이터형이 존재해야 합니다. 메모리 컬렉터 타입은 아무 것이나 쓸 수 있는데, MFC로 치면 CStringList라든지, CMapPtrToPtr 등이 될 수 있겠네요. 지정받은 타입의 인스턴스를 두 개 생성합니다. 또한, 해당 타입에 대한 input 포인터, output포인터를 선언하고, 초기화 루틴에서 각 인스턴스에 대한 포인터로 초기값을 넣어주면 되겠지요.


그리고 입력과 출력 동기화 오브젝트를 따로 선언하여, 입력과 출력 상황을 별도로 동기화하게 합시다. 이 때 입력 동기화 함수는 크리티컬 섹션을 사용했지만, 출력 동기화 함수는 뮤텍스를 사용했습니다. (Windows에서, 크리티컬 섹션의 경우 커널 오브젝트보단 부하가 덜하지만, 대기시간에 제한을 걸 수 없습니다. 뮤텍스는 크리티컬 섹션과 매우 유사한 커널 오브젝트이지만, 대기시간에 제한을 줄 수 있는 점이 다릅니다.) 이것은, 일반적인 프로그램에서 데이터를 수집하는 것은 여러곳에서 동시다발적으로 이루어지지만, 수집된 데이터를 실제로 처리하는 것은 동시다발적으로 일어나지 않거나, 여러 스레드에서 작업을 하더라도 굳이 스레드 대기까지 시켜가며 수집된 데이터를 처리할 필요는 없는 경우가 많기 때문입니다. 결국 m_mtOutput는 아예 사용하지 않거나, 사용하더라도 다른 스레드에서 데이터를 처리하고 있다면 대기하지 않고 바로 리턴하도록 처리하면 대부분의 경우 문제가 없습니다. 이것을 위의 투/개표 상황에 응용하면, 투표는 꼭 여러사람이 한꺼번에 하게 되지만, 개표의 경우는 (충분한 속도만 나온다면) 한 명이 전담해도 문제없는 것과 마찬가지입니다. 추가 인원이 필요하다 하여도, 꼭 두 사람에게 균등하게 데이터가 배분되어야 할 필요는 없는 것입니다. 노는 인원은 다른 일을 시켜야지, 할일없이 놀게(=블러킹 상태) 하면 안되겠지요.


물론 프로그램 특성에 따라, 데이터 입력보다 출력에 대부분의 부하가 집중되는 경우에는, 다른 전략을 사용해야 하겠지요. 이것은 나중에 기회가 되면 더 다루어 보겠습니다.


다음으로, 생성자, 소멸자, Lock 메서드들입니다.

  CSwappableCollector()
 {
  m_pcollInput = &m_coll[0];
  m_pcollOutput = &m_coll[1];
 }
 virtual ~CSwappableCollector(){};

 void LockInput() {m_csInput.Lock();};
 void UnlockInput() {m_csInput.Unlock();};


 // 아래 두 method는 Output Queue를 동시에 사용하는 일이 없을 경우 쓰지 않아도 됨
 bool LockOutput() {return !!m_mtOutput.Lock(0);};
 bool UnlockOutput() {return !!m_mtOutput.Unlock();};

생성자에서는, Attributes로 선언된 메모리 오브젝트 인스턴스 두 개의 포인터를 각각 데이터 입력, 출력 용 포인터로 초기화시킵니다.

동기화 Methods에서는 위에서 설명한 것처럼 각각 크리티컬 섹션과 뮤텍스 오퍼레이션을 통해 동기화를 구현합니다. 특히 LockOutput과 UnlockOutput의 리턴값이 bool인데, 이것은 다른 스레드에서 출력데이터 처리를 하고 있는지를 알기 위해서입니다. VC의 크리티컬 섹션으로는 이런 처리를 할 수 없음을 위에서 밝혔습니다. LockInput이 실패하면, 타 스레드에서 출력데이터 처리를 하고 있다는 의미이므로, 현재의 스레드는 그냥 즉시 다른 일을 하면 된다는 뜻이지요. m_mtOutputLock(0)에서 "0"이 대기시간입니다. 전혀 대기를 하지 않고, 동기화 오브젝트 소유의 가능성만을 타진한다는 것.


마지막으로, 포인터 교환 및 포인터 리턴 메서드들입니다.

  void Swap()
 {
  LockInput();
  if (m_pcollInput == &m_coll[0])
  {
   m_pcollInput = &m_coll[1];
   m_pcollOutput = &m_coll[0];
  }
  else
  {
   m_pcollInput = &m_coll[0];
   m_pcollOutput = &m_coll[1];
  }
  UnlockInput();
 }


 objT* GetInputColl() {return m_pcollInput;};
 objT* GetOutputColl() {return m_pcollOutput;};


};

Swap()메서드가 바로, 위에서 언급한 투표함을 바꿔치기하는 행위입니다. 데이터 복사를 전혀 하지 않고, 포인터가 지시하는 메모리 주소만 바꿔치기하는 것입니다. 투표함 바꿔치기라기보단, 투표함에 붙은 "유권자 투표용 투표함", "개표자 처리용 투표함"의 라벨만 바꿔치기하는 쪽에 더 가까울지도 모르겠네요.


GetInputColl(), GetOutputColl()은 각각의 메모리 컬렉터의 포인터를 얻고자 할 때 쓰는 메서드입니다. 포인터 Attributes를 public으로 선언하면 필요없는 메서드이지만 개인적으로 public attributes를 최소화하는 쪽을 선호하고 있어서 이렇게 적었습니다.


자, 이제 템플릿은 완성되었습니다. (가져다 쓰실 분은 첨부파일을 받으시면 됩니다.) 사용법을 알아보겠습니다. 편의상 템플릿의 데이터형이 될 메모리 컬렉터는 CStringList를 사용하였습니다.

// 선언

CSwappableCollector<CStringList> scString;


// 데이터 입력 Thread

 scString.LockInput();
 scString.GetInputColl()->AddTail("TTT");
 scString.UnlockInput();


// 데이터 출력(처리) Thread

 if (scString.LockOutput())
 {
  scString.Swap();

  while (scString.GetOutputColl()->GetCount() > 0)
  {
   CString &strTmp = scString.GetOutputColl()->GetHead();
   printf(strTmp);
   scString.GetOutputColl()->RemoveHead();
  }

  scString.UnlockOutput();
 }

당연한 얘기지만, 데이터 출력(처리)가 동시에 여러 스레드에서 이루어지지 않는다면, 동기화 관련 메서드(LockOutput, UnlockOutput)를 호출하는 부분은 사용하지 않는 것이 좋습니다. 쌓인 데이터를 타이머 등으로 일정시간마다 처리하는 형태가 보통 그런 경우입니다. 일반적인 타이머는 핸들러 함수가 중복호출되지 않기 때문입니다.  



자, 그럼 이것을 어디에 이용하느냐? 저는 이것을 UDP 동영상 스트리밍 처리에 이용했었습니다. UDP데이터는 TCP와는 다르게 여러 곳으로부터 데이터를 받아들일 수 있습니다. 특히 IOCP등을 이용할 경우에는 적절히 여러 스레드로 데이터가 분배되어 들어오지요. 이렇게 들어오는 데이터들을 순차적으로 가공한 뒤 동영상 플레이어에 전달하고자 할 경우, 데이터 입력 상황과 데이터 출력(처리)상황이 뚜렷하게 갈리게 되므로 이 템플릿을 적용하는 데에 무리가 없다 하겠습니다.



결론


용도 : 제한된 수의 자료구조를, 입출력 상태구분이 뚜렷한 여러 스레드에서 한꺼번에 이용하고자 할 경우.

장점 : 입력과 출력에 대해 별도의 프로세스 동기화를 하므로, 동일한 동기화 오브젝트를 사용하는 것보다 처리효율이 높다. 출력 데이터를 동시에 여러 스레드에서 다루지 않는 경우, 동기화 회수 및 블러킹 시간을 줄일 수 있다. 입출력 메모리를 서로 교환할 때 실제 데이터 복사를 하지 않는다. 

단점 : MFC 동기화클래스를 이용하므로 MFC 환경에서만 쓸 수 있다. 데이터 처리부에서 호출해야 하는 메서드의 수가 많다.  

향후 개선점 : 1. 동기화 오브젝트까지 템플릿으로 입력받게 하면 다른 플랫폼에서도 쓸 수 있다. 2. 데이터 출력용 동기화 오브젝트의 사용여부 또는 대기시간도 템플릿으로 입력받도록 처리하면 쓰임새를 더 넓힐 수 있다. 3. LockOutput 메서드와 Swap메서드를 결합하여 좀 더 사용법이 쉬워지게 할 수 있다.

: