멀티스레드 입출력에 최적화된 버퍼 템플릿 - 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메서드를 결합하여 좀 더 사용법이 쉬워지게 할 수 있다.

:

베이비아프리카 백일사진 촬영기..

세상만사/사는 이야기
2008. 8. 24. 17:56, Posted by ScottRhee

우리 준서가 드디어 백일이 되었습니다.

사실 백일은 지난 달 말에 이미 지났는데, 110일~120일 정도 이후에 촬영해야 사진이 더 예쁘게 나온다고 해서 좀 기다렸습니다. 특히 엎드려서 팔로 버티고 앞을 쳐다볼 수 있을 정도로 아기가 자라야만 쉽게 촬영이 가능하다고 합니다. 우리 준서도 딱 100일쯤 지났을 때에는 엎드려서 팔로 버티는 자세를 잘 못하더니, 사진 찍을 때쯤에는 매일 하는 일이 그 자세일 정도로 금방 성장하더군요. ^^

사용자 삽입 이미지

스투디오는 일산 근처의 파주 출판단지에 있었습니다. 번화가가 아니고 출판사 등의 사무실이 많이 입주해있는 곳이라, 떠들썩하지 않고 아주 좋더군요. 고급 팬션같이 생긴 오피스텔들이 줄지어 서있는데, 마치 유럽의 작은 도시에 온 듯한 느낌이 들었습니다. 다만 단지의 넓이가 넓고 건물 생김새들이 비슷비슷해서 정확한 사무실 위치를 찾기가 어려운 게 옥의 티. 홈페이지 약도상에 내비게이션으로 찍고 오면 뱅글뱅글 돌게 된다고 오는 길까지 정확하게 안내해 주셨건만, 또 헤매고 말았습니다.-_-;; 다행히 예약을 하러 갔을 때 한 번 헤매봐서 그런지 실제 촬영을 하러 갔을 땐 한 번에 찾기 성공 -_-b

사용자 삽입 이미지
아담한 2층집 구조의 스투디오.. 사진에서 보시는 것처럼 맑고 밝은 빛이 가득 들어옵니다. 자연광의 도움을 많이 받아 한껏 자연스런 사진을 찍을 수가 있었습니다.

스투디오는 2층 구조로 되어 있는데, 2층 방은 상담실 및 대기실이고, 실질적인 촬영은 1층 거실과 반지하실에서 이루어졌습니다. 지하실로도 자연광이 충분히 들어옵니다. 위와 아래에 있는 사진이 반지하실에서 찍은 것입니다.

사용자 삽입 이미지

준서가 백일 애기치고는 몸집이 좀 큰 편이라 걱정을 했는데, 아기 사진을 전문으로 찍는 곳이다보니 다양한 사이즈의 옷이 있었습니다. 다행스럽게도 돌이 된 애기들이 입는 옷을 입고 촬영을 할 수 있었습니다. 옷도 멋지지만 모자들도 아주 절묘합니다. 갓난 아이라서 머리숱이 많지가 않은데, 적절한 모자신공을 발휘하니 아이가 아주 이뻐보입니다.

사용자 삽입 이미지
촬영 컨셉은 대충 여섯 가지 정도를 진행했습니다. 아기가 컨디션이 좋으면 다양한 레퍼토리를 적용해보고, 아기가 빨리 지치거나 잠을 자게 되면(^^;;) 아무래도 촬영량이 떨어지게 되는데, 준서는 참 기특하게도 여러가지 컨셉을 잘도 넙죽넙죽 소화해 주었습니다. 제 생각에는 스투디오 사장님이랑 누님의 애기 컨트롤 기술(?)이 너무 좋아서 준서도 신이 나서 촬영을 열심히 해준 것 같습니다. 인형도 쥐어주고 포즈도 바꿔주고 옷도 갈아입혀주는데다 아이가 집중을 안 할때마다 하나씩 꺼내 쓰시는 비장의 아이 웃기기 기술을 선보여주시니 애기가 너무 좋아해서 옆에서 구경하는 저랑 마눌님도 너무 즐겁게 촬영을 할 수 있었습니다.

사용자 삽입 이미지

결혼식을 올릴 때까지는 우리 두 사람이 주인공이었지만, 이제부터 주인공은 아기입니다. 가족 사진은 이렇게 딱 한 컨셉으로만 촬영해 주시더군요ㅠ.ㅠ 부모의 인생이란 게 이런 건가 봅니다. ^^;; 이 컨셉으로 여러 장 촬영을 해 주셨는데, 윗 사진은 가장 코믹하게 찍힌 컷입니다. 아기 표정이 아주 예술입니다. ^^;;

사용자 삽입 이미지

촬영은 약 한시간 반 정도의 시간이 소요되었습니다. 사장님께서 말씀하시길, 진행이 너무 쉽게 되었다고 하시네요. 우리 준서도 수고 많이 했지만, 사장님 내외분^^께서 리드를 아주 잘 해주신 덕분에 아주 즐겁게 일을 마칠 수 있었습니다. 촬영후 애기는 잠들고 마눌님이 미리 준비한 과일을 맛있게 나눠먹으면서 즐거운 여행이 끝이 났습니다. 여러 번 더 하고 싶을 정도로 재미있고 즐거운 촬영이었어요. 특히 사장님 내외분의 절묘한 호흡과 금슬이 부러웠답니다. 이 글을 보실 수 있을지는 모르겠습니다만 다시 한 번 감사의 말씀을 드립니다. 이 글을 보시는 여러분들 모두 행복하시고, 아기와 함께 즐거운 추억 많이많이 만드세요~~!!



: