블로그 이미지
대갈장군

calendar

1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31

Notice

2010. 3. 18. 06:25 프로그래밍/C++

불과 몇년 전만 하더라도 멀티 스레드를 사용하는 프로그램은 드물었다. 오늘날의 인터넷 서버 프로그램은 다수의 클라이언트 연결을 위해 여러개의 스레드를 이용한다. 효율과 생산성을 최대화 하기 위해서 트랜잭션 서버들은 분리된 여러개의 스레드를 이용하곤 한다. GUI 프로그램 또한 오랜 시간이 걸리는 작업은 분리된 스레드를 통해 백그라운드로 작업을 하면서 동시에 사용자의 입력은 실시간을 받아들인다. 이런식으로 나열하면 수도 없이 많다. 그만큼 멀티 스레드는 보편화 되었다는 것.

기존의 C++ Standard는 멀티 스레드에 대해 언급조차 하지 않는다. 심지어는 프로그래머에게 멀티 스레드 C++ 프로그램이 작성 가능한지 조차 알려주지 않는다. (과거 C나 C++ 은 멀티 스레드 환경에서 설계 되지 않았기 때문) 물론 기본적인 함수를 이용한 멀티 스레드 프로그램이 작성 불가능 한것은 아니지만, 프로그래머는 그럼에도 불구하고 운영체제가 지원하는 스레드 관련 라이브러리를 이용해서 작성한다. 자, 이것은 두가지 문제를 가지고 있다. 우선, 이 운영체제에서 제공하는 라이브러리들은 거의 C 라이브러리들인데다가 C++에서 사용시에는 주의해야 한다. 그리고 각각의 운영체제는 자신만의 멀티 스레드 조작 함수를 사용한다는 것이다. 이렇게 만들어진 코드는 우선 'Non-Standard(비기준)' 인데다가 'Non-Portable(비호환성)' 이다. 운영체제마다 쓰는 함수가 다르니 윈도우에서 돌리던 프로그램 유닉스에서 돌리려면 코드를 다 뜯어 고쳐야 한다는 말... 고로, Boost.Therads 라는 라이브러리를 만들었다는 말이다. 이 두 문제를 한방에 해결하기 위해서...

Boost는 C++ Standards Committee Library Working Group의 멤버들이 C++의 새로운 라이브러리를 만들이 위해 시작되었다. 현재 대략 2000명이나 된다구. 수많은 boost의 라이브러리를 thread-safe 하게 사용하기 위해서 바로 이 Boost.Threads가 만들어 졌다...

많은 C++ 전문가들이 Boost 디자인을 위해 생각들을 내어놓았고 인터페이스는 바닥부터 차근 차근 설립한 기초공사부터 시작했단다. 걍 원래 있는 C 스레드 API 함수를 가져다 쓰는게 아니라 이말이다. 많은 C++의 특징들이 흡수되었고 (예로 생성자, 파괴자, 함수 객체, 템플릿등) 인터페이스를 보다 유연하게 만들었다. 현재 Boost가 돌아가는 환경은 POSIX, Win32, Mac 등이래요. 뭐 거의 크로스 플랫폼을 달성했다고 봐도...

Thread Creation

boost::thread는 std::fstream 클래스가 파일을 표현하는 것과 같은 방식으로 스레드의 실행을 표현한다. Default 생성자가 현재의 실행 스레드를 나타내는 객체를 생성한다. 오버로딩된 생성자는 입력 변수가 없고 리턴하는 것이 없는 함수 객체를 받는다. 이 생성자는 새로운 실행 스레드를 시작하는데 이는 결국 함수 객체의 호출이며 해당 함수의 호출이다.

첫눈에 보기에는 이 방식의 디자인이 기존 C의 방식보다 후져 보인다. 왜냐면 기존 C는 void 포인터를 새 스레드 생성시 데이터를 넘겨주기위해 사용할 수 있기 때문이다. 하지만 Boost.Threads는 함수 포인터 대신 함수 객체를 사용하기 때문에 이 함수 객체가 스레드에 필요한 데이터를 소유 할 수 있다. 이 접근법은 타입 점검을 정확히 하며 보다 유연하다. 다른 함수 라이브러리들, 예를 들어 Boost.Bind와 같은 것과 연동되면 이 방식은 어떤 형태의 데이터라도 (많든 적든) 새로 만들어진 스레드로 전달하기가 매우 쉽다. 

현재로써 Boost.Threads로 할수 있는건 많지는 않다. (이 글이 작성된게 2002년인걸 감안하면 이렇게 말할만도 하다.) 사실 딱 두개의 연산자만 사용할 수있다. (지금은 바뀌었겠지?) == 과 != 연산자를 이용하면 간단하게 같은 스레드인지 아닌지 확인 가능하다. 그리고 boost::thread::join을 이용하면 스레드가 작업을 다 할때까지 기다릴수 있다. 다른 스레드 라이브러리들은 스레드와 관련된 다른 작동을 할 수 있게 도와 준다. (예를 들면 우선순위 설정) 

아래 리스트 1은 아주 간단한 boost::thread 클래스의 사용을 보여준다. 새로운 스레드가 생성되고 이 스레드는 아주 간단한 hello() 함수를 호출한다. 이걸 하는 동안 main 스레드는 대기한다. 
#include <boost/thread/thread.hpp>
#include <iostream>

void hello() { std::cout << "Hello world, I'm a thread!" << std::endl; } int main(int argc, char* argv[]) { boost::thread thrd(&hello); thrd.join(); return 0; }

VS 2008으로 위 코드를 돌려봤더니 잘 돌아간다. 그리고 실행중에 디버깅을 해보면 아래 그림에 나온 것 처럼 hello() 함수 호출후 join 함수로 스레드 실행의 완료를 기다린후 return 0; 바로 직전에 브레이크 포인트를 자세히 보면 친절하게도 '이 프로세스나 스레드는 바로 직전에 변경되었습니다.'라고 알려준다. 참 친절하신 VS 2008. :)

 

Mutexes

멀티 스레드의 최대 약점은 자원 경쟁에 의한 오작동이다. 프로그램 짜본 사람이라면 누구나 알것이다. 이것은 두개 이상의 스레드가 동시에 공유된 자원에 접근하여 데이터를 변경할때 발생하는 데이터의 부정확성과 불일치를 말한다. 이것을 방지하기 위한 여러방법중 하나가 바로 Mutex 다른말로 하면 Mutual Exclusion (상호 배타)이다. 이 뮤텍스는 오직 하나의 스레드만이 공유자원에 지정된 시간에 접근하도록 만든다. 고로, 스레드가 공유 자원에 접근하기 위해서는 우선 뮤텍스를 'lock' 해야 한다. 즉, 공유자원이라는 집안에 들어가서 문을 걸어 잠그는 것이다. (다른 놈 못들어오게) 

뮤텍스의 컨셉은 여러가지가 있는데 Boost.Threads가 지원하는 가장 큰 두개의 카테고리는 Simple Mutex와 Recursive Mutex다. Simple Mutex는 말그대로 단순한 뮤텍스로써 오직 한번만 잠글수 있다. 만약 자신이 잠근 뮤텍스를 또 다시 잠글려고 하면 바로 데드락이 걸리고 이것은 '무한 대기(데드락)' 상태를 유발한다. 반면 Recursive Mutex (한국어로 말하면 재귀 뮤텍스이군)의 경우는 하나의 스레드가 뮤텍스를 여러번 잠글수 있으나 대신 반드시 잠근 횟수만큼 풀어야 다른 스레드가 공유자원을 사용할 수 있다는 것이다. 집에 들어와서 자물쇠를 현관문에 100개 달았으면 100개 다 풀어야 남이 들어온다는 이야기.

이 두가지의 카테고리 내부에서 또 여러가지 형태로 나뉘는데 이것은 어떤 식으로 뮤텍스를 잠그느냐에 따라 나뉜다. 세가지 방법으로 뮤텍스를 잠글수 있는데,
  1. 다른 스레드가 뮤텍스를 잠그지 않을때까지 대기
  2. 만약 다른 스레드가 뮤텍스를 잠궈놓은 상태라면 바로 리턴하기
  3. 다른 스레드가 뮤텍스를 잠그지 않을때까지 대기 하거나 혹은 일정 시간만 대기 하다가 리턴
최고의 뮤텍스 타입은 아무래도 recursive 타입인데 이 것은 위의 세가지 잠그기 방식을 모두 지원하기 때문이라는데... 반면 좋은 대신 오버헤드가 크다는 단점이 있다. 아무튼, 총 여섯 가지의 뮤텍스를 사용자가 선택가능하며 다음과 같다. 

boost::mutex
boost::try_mutex
boost::timed_mutex
boost::recursive_mutex
boost::recursive_try_mutex
boost::recursive_timed_mutex.

데드락은 매번 뮤텍스를 잠글때마다 잠근 횟수만큼 풀지 않았을때 발생한다. 이것이 가장 흔한 데드락의 한 종류인데 Boost.Threads는 이 데드락을 최대한 최소화 시킨다. (발생 할 수는 있다는 이야기) 어떤 뮤텍스이건 간에 직접적인 뮤텍스 잠금 및 해제 함수는 없다. 즉, 간접적인 방식으로 잠그고 푸는데 이것이 안전성을 높인다는 이야기. 이걸 Scoped Lock 이라고 한다는데 Douglas Schmidt, Michael Stal, Hans Rohnert, and Frank Buschmann. Pattern-Oriented Software Architecture Volume 2 Patterns for Concurrent and Networked Objects (Wiley, 2000) 를 참조하라고 해놨다. 헤헤, 거의 논문 수준이다. :)

아무튼, C++이 기본적으로 예외 발생시나 객체 파괴시 항상 파괴자를 호출하므로 파괴자에 뮤텍스를 풀어주는 장치를 둠으로써 이런 데드락을 줄인다는 이야기 란다. 다만 주의 할 것은 이 Scoped Lock 패턴이 비록 풀어주는 건 확실히 풀어주지만 예외가 발생해서 풀어졌을 경우 공유자원이 변경되었을 가능성이 있단다. 
 
아래의 코드는 간단한 뮤텍스 사용법을 보여준다.
#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <iostream>

boost::mutex io_mutex;

struct count { count(int id) : id(id) { }

void operator()() { for (int i = 0; i < 10; ++i) { boost::mutex::scoped_lock lock(io_mutex); std::cout << id << ": " << i << std::endl; } }

int id; }; int main(int argc, char* argv[]) { boost::thread thrd1(count(1)); boost::thread thrd2(count(2)); thrd1.join(); thrd2.join(); return 0; }

위의 코드를 보면 thrd1과 thrd2라는 스레드를 생성하여 메인 스레드는 이 두 스레드가 끝날때까지 대기한다. 이 두 스레드는 count라는 구조체를 호출하는데 이 구조체 내부에 std::cout 함수가 호출되어 0에서 9까지의 숫자를 출력한다. 재미있는 것은 이 std::cout은 이른바 공유 자원이다. 화면에 글자를 출력해내는 함수는 한순간에 오직 하나의 스레드에 의해서만 점유되어야 함이 마땅하다. 고로 이 프로그램을 돌려보면 다음과 같이 각각의 스레드에 대해 0에서 9까지의 출력이 차례대로 나온다. 


헌데 만약 위 코드에서 뮤텍스를 잠그는 명령을 쏙 빼버리면 다음과 같은 결과가 나온다. 


잘 보면 thrd1의 "1"을 출력한후 스레드가 변경되어 thrd2의 "2: "를 출력한 후 다시 thrd1로 변경되어 ": 0"를 출력한 후 또 다시 thrd2로 변경되어 "0"을 출력한다. 이렇게 규칙성있게 출력되는 이유는 분명히 CPU가 공정하게 스레드에게 실행 시간을 나눠주기 때문일 것이다. 두 스레드가 같은 우선순위를 가지므로 둘이 똑같이 CPU 시간을 나눠 받는 것이다. 

그리고 위 코드를 다시 보면 함수 객체를 작성하는데 이거 매번 하려면 굉장히 불편하다. 이럴때 사용할 수 있는 것이 Boost.Bind란다. 다음 코드를 보면 새 함수 객체를 바인딩을 통해 값을 전달하여 생성하는 예를 보이고 있다. 

Listing 3: Using the Boost.Bind library to simplify the code in Listing 2

// This program is identical to
// listing2.cpp except that it
// uses Boost.Bind to simplify
// the creation of a thread that
// takes data.

#include <boost/thread/thread.hpp> #include <boost/thread/mutex.hpp> #include <boost/bind.hpp> #include <iostream>

boost::mutex io_mutex;

void count(int id) { for (int i = 0; i < 10; ++i) { boost::mutex::scoped_lock lock(io_mutex); std::cout << id << ": " << i << std::endl; } } int main(int argc, char* argv[]) { boost::thread thrd1(boost::bind(&count, 1)); boost::thread thrd2(boost::bind(&count, 2)); thrd1.join(); thrd2.join(); return 0; }


Condition Variables

종종 걸어 잠그기 만으로는 충분하지 않은 경우가 있다. 즉 뮤텍스만으로는 해결 안되는 상황을 말한다. 어떤 자원이 있는데 그 자원을 사용하려면 반드시 특정 상태가 되어야 한다고 치자. 예를 들어 임의의 데이터가 네트워크를 통해서 들어오기를 기다려야 하는 상황이 있다면 뮤텍스는 이런 형태의 점검을 지원하기에는 적합하지 않다는 말이다. (아래를 보면 이해가 될것, 왜 뮤텍스로는 부족한지) 그래서 다른 형태의 동기화가 필요한데 이것이 바로 '조건 변수 (Condition Variable)'를 이용하는 것이란다.

하나의 조건 변수는 항상 뮤텍스와 공유 자원 사이에 접합 지점에서 사용이 된다. 하나의 스레드가 먼저 뮤텍스를 잠그고 난 다음 공유자원이 적합한 상태인지를 체크하게 된다. 만약 해당 상태가 아직 원하는 상태가 아니라면 스레드는 대기한다. 이 대기 상태에서 스레드는 뮤텍스를 풀어줌으로써 다른 스레드가 공유자원에 대해 남은 일을 처리하여 적합한 상태 (Ready 상태)로 변경할 수 있게 된다. 이것이 바로 조건 변수가 필요한 이유. 무한 대기가 아닌 조건 대기. 또한 이것은 스레드가 대기 상태에서 복귀할때 바로 뮤텍스를 걸어 잠그는 것까지 한다. 다른 스레드가 공유자원에 대한 작업을 마쳐서 조건 변수의 상태를 변경하게 되면 이것을 즉각적으로 대기하던 스레드에게 알려준다. 그로써 대기중인 스레드는 바로 복귀함과 동시에 완료된 공유자원에 뮤텍스를 이용해 잠근다. 

생각보다 현명한데? :) 조건 변수라고 하길레 그냥 정적 변수 하나 선언해 놓고 true / false 값 판단인줄 알았는데 제법 인공지능을 가지고 있다. 
#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/condition.hpp>
#include <iostream>

const int BUF_SIZE = 10; const int ITERS = 100;

boost::mutex io_mutex;

class buffer { public: typedef boost::mutex::scoped_lock scoped_lock; buffer() : p(0), c(0), full(0) { } void put(int m) { scoped_lock lock(mutex); if (full == BUF_SIZE) { { boost::mutex::scoped_lock lock(io_mutex); std::cout << "Buffer is full. Waiting..." << std::endl; } while (full == BUF_SIZE) cond.wait(lock); } buf[p] = m; p = (p+1) % BUF_SIZE; ++full; cond.notify_one(); }

int get() { scoped_lock lk(mutex); if (full == 0) { { boost::mutex::scoped_lock lock(io_mutex); std::cout << "Buffer is empty. Waiting..." << std::endl; } while (full == 0) cond.wait(lk); } int i = buf[c]; c = (c+1) % BUF_SIZE; --full; cond.notify_one(); return i; } private: boost::mutex mutex; boost::condition cond; unsigned int p, c, full; int buf[BUF_SIZE]; };

buffer buf;

void writer() { for (int n = 0; n < ITERS; ++n) { { boost::mutex::scoped_lock lock(io_mutex); std::cout << "sending: " << n << std::endl; } buf.put(n); } } void reader() { for (int x = 0; x < ITERS; ++x) { int n = buf.get(); { boost::mutex::scoped_lock lock(io_mutex); std::cout << "received: " << n << std::endl; } } } int main(int argc, char* argv[]) { boost::thread thrd1(&reader); boost::thread thrd2(&writer); thrd1.join(); thrd2.join(); return 0; 

}위 코드의 클래스는 FIFO 버퍼를 추상화했는데 내부 private 멤버 변수인 mutex를 사용해서 스레드에 안전하게 설계되었다. put과 get 함수는 condition variable을 사용하여 스레드가 적합한 상태로 공유자원이 변경되도록 기다리게 했다. 두 스레드가 만들어지면 하나는 100개의 정수를 이 버퍼에 넣고 다른 하나는 하나씩 빼낸다. 허나 이 버퍼는 최대 10개까지만 대기할 수 있으므로 두 스레드스 서로 기다려 준다. 잘보면 전역변수로 io_mutex를 선언하여 std::out에 대해서 뮤텍스를 사용함을 알수 있다. 

Thread Local Storage

 이 놈은 예전에 내가 다른 글에서 언급한 놈이다. http://diehard98.tistory.com/entry/프로그램-프로세스-스레드 를 보면 왜 TLS가 필요한지 설명하고 있다. 기본적으로 함수들은 재진입을 염두하지 않는다. 고로 하나의 스레드가 호출중인 함수를 다른 스레드가 또 호출하면 '불안정한' 상태가 될 수 있다는 의미. 재진입을 고려하지 않은 함수는 정적 데이터를 유지하는데 대표적인 예로 std::strtok가 있다. 이 놈은 재진입이 불가한데 왜냐면 함수가 호출되면 실행 동안에 정적 변수에 값을 저장해 놓기 때문이다. (고로 다른 스레드가 실행 와중에 다시 그 함수를 호출하여 사용하면 같은 정적 변수에 또 다른 값을 덮어 씌워 버릴것이다.)

이런 재진입이 불가한 함수들은 두가지 방법으로 재진입이 가능한 함수로 바꿀수 있다. 우선 첫번째는 포인터나 레퍼런스를 받아서 정적 변수를 사용하던 방식을 바꾸는 것이다. 예를 들어 POSIX의 strtok_r 함수는 std::strtok의 재진입을 가능하게 만든 함수로 이 함수는 입력 변수로 정적 변수를 사용하는 대신 char** 을 받는다. 이 해결법은 쉽고 좋다. 그러나 이렇게 하면 모든 공개된 인터페이스 (호출 방식)을 바꿔야 한다. 즉, 많은 코드의 변화를 가져온다는 이야기. 이 방법 말고 다른 방법인 두번째 방법이 바로 스레드에 독립적인 저장 공간을 주는 방식이다. 스레드의 독립적인 저장공간을 영어로 Thread Local Storage 혹은 Thread-Specific Storage라고 한다.

TLS는 각 스레드 별로 할당되는 고유 공간이다. 멀티스레드 라이브러리는 이 독립공간을 임의의 스레드가 접근할수 있도록 인터페이스를 제공한다. 고로 각각의 스레드는 스레드 객체의 고유 데이터를 소유하게 된다. 이 말은 다른 스레드가 접근 할 수 없다는 말이고, 즉, 자원 경쟁으로부터 자유롭다는 말. 문제는, 이 TLS가 일반 데이터 엑세스보다 느리다는 점. 하지만 첫번째 방법보다는 편한데 왜냐면 인터페이스를 안바꿔도 되니까 코드 변경이 필요없기 때문이다.

Boost.Threads는 boost::thread_specific_ptr이라는 스마트 포인터를 이용해서 TLS에 각 스레드가 접근 할 수 있도록 도와준다. 스마트 포인터란 참조된 횟수를 스스로 계산하여 자폭하는 영리한 포인터를 말한다. 각 스레드는 우선 이 스마트 포인터의 객체에 접근하는데 이때 이 값이 NULL인지 먼저 확인해야 한다. NULL이면 초기화가 필요하다는 말. 그리고 Boost.Threads 라이브러리는 스레드 종료시 이 TLS를 알아서 청소해준다. 왜냐면 스마트 포인터니까~
#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/tss.hpp>
#include <iostream>

boost::mutex io_mutex; boost::thread_specific_ptr<int> ptr;

struct count { count(int id) : id(id) { }

void operator()() { if (ptr.get() == 0) ptr.reset(new int(0));

for (int i = 0; i < 10; ++i) { (*ptr)++; boost::mutex::scoped_lock lock(io_mutex); std::cout << id << ": " << *ptr << std::endl; } }

int id; }; int main(int argc, char* argv[]) { boost::thread thrd1(count(1)); boost::thread thrd2(count(2)); thrd1.join(); thrd2.join(); return 0; }

위 코드를 실행해 보면 각 스레드 별로 1에서 10까지 출력한다. 그리고 당근 std::out은 뮤텍스로 보호!

Once Routines

이제 남은 것은 딱 하나. 어떻게 생성자를 멀티스레드에 안전하게 만드는가다. 예를 들어 전역으로 사용되는 객체가 있다면 멀티스레드 환경에서 주의해야 할 것은 이 객체가 오직 한번만 생성자를 호출해야 하며 스레드들이 동시에 생성자를 호출하는 것을 막아야 한다.

이 것을 해결하는 방법은 이른바 "Once routine" 이라는 방식의 사용이다. 이 것을 이용하면 해당 함수나 변수는 프로그램에 의해 오직 한번만 호출된다. 만약 다수의 스레드가 이 함수를 동시에 호출한다면 그들중 오직 하나만이 이 함수에 접근할 수 있다. 이것을 가능하게 해주는 방법은 바로 랩핑함수를 이용해서 한번 더 점검을 하는 거란다. 고로 멀티 스레드 환경에서 '오직 한번' 초기화하기 문제는 해결되었다. boost::call_once를 이용하면 되고 플레그 타입인 boost::once_flag를 BOOST_ONCE_INIT 매크로를 이용해 초기화 하면 된다네.

Listing 6: A very simple use of boost::call_once

#include <boost/thread/thread.hpp>
#include <boost/thread/once.hpp>
#include <iostream>

int i = 0; boost::once_flag flag = BOOST_ONCE_INIT;

void init() { ++i; }

void thread() { boost::call_once(&init, flag); } int main(int argc, char* argv[]) { boost::thread thrd1(&thread); boost::thread thrd2(&thread); thrd1.join(); thrd2.join(); std::cout << i << std::endl; return 0; }

뭐 위 코드를 보면 flag를 선언할때 BOOST_ONCE_INIT 매크로 사용했고 전역 변수 i를 두개의 스레드가 동시에 접근하는데 둘 다 call_once로 호출하므로 두 스레드중 오직 하나만 접근 하여 초기화 한후 값을 하나 증가 시킨다. 고로 다른 스레드는 해당 함수를 더 이상 호출 할 수 없으므로 출력 값으로는 1이 출력된다. 만약 두 스레드가 모두 변수에 호출가능 했다면 i 값은 2가 되어야 한다.

The Future of Boost.Threads

Boost.Threads를 위한 몇가지 계획이 있는데 추가될 놈으로 boost::read_write_mutex가 있단다. 이 것은 멀티 스레드들이 공유자원을 동시에 읽기 가능하게 하고 단, 쓸때는 한놈만 쓰게 한다. 그리고 추가로 boost::thread_barrier 라는 놈을 만들건데 이 놈은 특정 스레드들이 모두 특정한 위치까지 들어오도록 기다린단다. 흠... 용이하겠는데? 그리고 또 boost::thread_pool을 계획중인데 이 녀석은 스레드를 매번 생성 파괴하지 않고 짧은 실행을 비동기로 할 수 있게 해주는 놈이란다. 찾아보니 이 놈들 벌써 구현됬다... ㅋㅋㅋ

그리고 이 Boost.Threads는 차기 C++ 표준에 들어갈 수도 있다는 말과 함께 이 긴 글을 마무리한다...

추가로 참조된 책 및 웹사이트, 논문등

Notes

[1] The POSIX standard defines multithreaded support in what’s commonly known as the pthread library. This provides multithreaded support for a wide range of operating systems, including Win32 through the pthreads-win32 port. However, this is a C library that fails to address some C++ concepts and is not available on all platforms.

[2] Visit the Boost website at <http://www.boost.org>.

[3] See Bjorn Karlsson’s article, “Smart Pointers in Boost,” in C/C++ Users Journal, April 2002.

[4] Douglas Schmidt, Michael Stal, Hans Rohnert, and Frank Buschmann. Pattern-Oriented Software Architecture Volume 2 Patterns for Concurrent and Networked Objects (Wiley, 2000).


이제 내가 이야기를 할 차례다. 일단 이 글쓴이에게 감사하는 바이다. 2002년에 작성된 글이라 오래된 느낌이 있지만 매우 영리하고 정확한 표현으로 이해를 돕는다. 그리고 중간중간 효과적인 소스코드로 더더욱 이해가 쉽게 해주었다.

Boost 라는 그 자체에 대해서 경이로움을 느낀다. C++이 멀티스레드에 약하다는 것은 잘 알고 있었지만 그것을 이렇게 잘 커버해주는 이런 대형 라이브러리가 있는지는 정확히는 몰랐다. 대단하다...

스레드의 생성과 선언 및 관리가 운영체제마다 다르기 때문에 윈도우에서 작성한 코드는 유닉스에서 안돌아 가지만 이 Boost 라이브러리를 이용하면 코드 변경 없이 가능하겠다. 그리고 스레드마저 객체화하여 클래스로 표현하므로 객체 지향에도 걸맞는다. 

오버헤드가 좀 있겠지만 객체로 표현한다는 장점에 비하면 적지 않나 싶다. 이 글이 너무 길어지면 곤란하므로 여기까지만 쓰겠다. 

posted by 대갈장군
2010. 3. 11. 03:47 OpenGL
FBO는 OpenGL에서 사용하는 '외부에 그리기' 기술중 하나로써 나도 종종 사용한다. FBO를 사용함으로써 사용자는 다양한 작업이 가능해 진다. 

대표적인 FBO 사용예로 그려질 scene 내부에 또 다른 scene을 그려야 할 때다. 집안에 있는 TV를 표현할 때 종종 이 방법이 사용된다. FBO의 장점으로는 간단한 셋업과 사용 그리고 context switching이 필요없다는 점 (오버헤드 적음) 각종 버퍼 (depth buffer, stencil buffer, accumulation buffer) 등을 사용할 수 있어 유용하다는 점 등이다. 


1. 셋업

우선 다른 OpenGL 객체와 마찬가지로 FBO 핸들을 받을 변수를 선언해야 하고 그 변수에 핸들을 생성해서 대입해야 한다. glGenFramebuffersEXT() 함수의 첫번째 인자는 glGenTextures() 함수의 첫번째 인자처럼 몇개나 만들건지 지정해주는 인자다.
GLuint fbo;
glGenFramebuffersEXT(1, &fbo);
명령을 수행하려면 바인딩 부터 해야 한다. 바인딩을 함으로써 다음에 이어지는 명령들이 바인딩한 객체에 대해 수행되도록 한다.
glBindFramebufferEXT
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo);
첫번째 인자가 Target 인자 인데 GL_FRAMEBUFFER_EXT는 고정된 것이라고 보면 된다. 물론 나중에 확장이 되면 다른 형태가 추가 될수 있지만 현재로서는 저 인자가 고정이라고 보면 된다. 두번째 인자로 앞서 정의한 핸들을 넘겨주면 오케이.

2. 깊이 버퍼 (Depth buffer) 추가

FBO 객체 자체로는 할 수 있는게 없다. 왜냐면 걍 객체(식별표)이니까... 이제 이 식별표에 실제적인 저장 공간(버퍼)들을 추가해 줘야 한다. 알다시피 OpenGL이 기본적으로 제공하는 Frame buffer도 깊이 버퍼, 스텐실 버퍼 등등으로 구성되어 있다. 고로 FBO 객체에도 Renderable (렌더링 가능한) 버퍼를 추가 시켜 주어야 한다. 이 버퍼로 사용가능한 것이 텍스쳐 또는 렌더버퍼 중 하나다. 

렌더버퍼는 FBO의 offscreen rendering을 지원하기 위한 객체이다. 적절한 텍스쳐 포멧을 지원하지 않는 스텐실 버퍼나 깊이 버퍼를 표현하기 위해서 사용하는 것이 바로 렌더버퍼 객체다. 이 예제에서는 렌더버퍼를 FBO의 깊이 버퍼로 사용할 것이다. 

그러니까 요약하자면, FBO는 OpenGL이 기본적으로 가지고 있는 깊이 버퍼, 스텐실 버퍼, 색상 버퍼등을 '연결'할 수 있는 객체인데 이때 이 연결 가능한 놈들은 두 종류로써 하나는 일반 텍스쳐이고 다른 하나는 '렌더버퍼(Renderbuffer)'이다. 일반 텍스쳐의 경우 색상 버퍼로 사용할 수 있으나 깊이 버퍼나 스텐실 버퍼와 같이 텍스쳐로 표현할 수 없는 버퍼들은 렌더버퍼를 이용해서 FBO에 연결한다는 말인듯 하다. 

아무튼, 일단 렌더버퍼를 생성하는 방법은 다음과 같다.
GLuint depthbuffer;
glGenRenderbuffersEXT(1, &depthbuffer);
depthbuffer라는 변수 (핸들을 받기위한)를 선언하고 generation 함수를 통해 렌더버퍼를 생성해 낸다. (그래서 함수 이름도 glGenRenderbuffersEXT() 이다. 

이제 이렇게 만든 렌더버퍼를 바인딩 시킴으로써 다음에 이어지는 일련의 명령이 depthbuffer 렌더버퍼에 대해 실행되도록 한다.
glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, depthbuffer);
앞서 본 것과 마찬가지로 GL_RENDERBUFFER_EXT는 고정이라고 보면 된다. 그리고 두번째 인자로 앞서 만든 렌더버퍼의 핸들을 넘겨준다. 

자, 이제 해야 할 일은 이 깊이 버퍼의 크기를 지정해 주는 일이다. (폭과 높이 지정하기)
glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, 
                         width, height);
이 과정은 일종의 메모리 할당이라고 보면 되겠네. 한가지 알아둘 것은 이 렌더버퍼는 기본 RGB/RGBA 저장용으로 사용가능하며 스텐실 버퍼로써 연결도 가능하단다. RGB/RGBA 저장 공간으로 사용될 수 있다는 점은 조금 신선하긴 하다만 텍스쳐를 사용하는 것이 더 유연할 것이라 생각된다.

다음은 이렇게 실제적으로 메모리를 할당한 깊이 렌더버퍼를 우리가 앞서 만든 fbo 객체에 가져다 붙이는 것이다. 
glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, 
                             GL_RENDERBUFFER_EXT, depthbuffer);
위 명령이 좀 복잡해 보여도 실은 단순하다. 현재 바인딩 되어 있는 FBO 객체 (fbo)에 렌더버퍼 (depthbuffer)를 바인딩 하라.. 라는 말이다.

3. 렌더링 텍스쳐 추가하기

앞서 두 단계를 수행했지만 아직까지 FBO에 색상 버퍼를 추가하지 않았다. 이제 색상 버퍼를 추가 해야 하는데 이 색상 버퍼가 사실상 '내 눈에 보여질' 놈이다. 

이 색상 버퍼를 추가하는 방법은 두가지가 있다.
1. 색상 렌더버퍼를 FBO의 색상버퍼로 연결
2. 일반 텍스쳐를 FBO의 색상버퍼로 연결

첫번째 방법도 사용하기는 하지만 우리는 두번째 방법을 사용할 것이다. (앞서 2번에서 설명했듯이 렌더버퍼는 기본 RGB/RGBA를 저장하기 위한 버퍼로도 사용가능하다고 했다. 하지만 분명 일반 텍스쳐 보다는 범용성이 떨어질 듯하다.)

텍스쳐를 FBO에 연결하기 위해서는 당연히 텍스쳐 생성부터 해야 한다.
GLuint img;
glGenTextures(1, &img);
glBindTexture(GL_TEXTURE_2D, img);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8,  width, height, 0, GL_RGBA, 
             GL_UNSIGNED_BYTE, NULL);
이 과정은 일반적인 텍스쳐 생성 과정이다. 이때 주의 해야 할 한 가지 포인트는 폭과 넓이가 앞서 만든 깊이 렌더버퍼의 크기와 같아야 한다는 매우 상식적인 것이다. 

자, 이제 색상 버퍼로 사용될 텍스쳐를 만들었으므로 우리가 앞서 만든 FBO 객체에 연결하자.
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, 
                          GL_TEXTURE_2D, img, 0);
이 명령이 어렵게 보여도 실제로는 그닥 어렵지 않다. 우선 굵은 글자로 표시된 GL_COLOR_ATTACHMENT0_EXT가 좀 어렵게 보일지 모르나 이것은 OpenGL에게 어떤 color attachment point에 연결할 지를 명시해주는 인자일 뿐이다. FBO는 동시에 여러개의 색상 버퍼를 가질 수 있기 때문에 어떤 포인트에 연결할 지를 명시해야 한다. 하나의 색상 버퍼만 사용한다면 저 숫자 0은 당신에게 아무런 중요성이 없는 단순한 표식일 뿐이다. 그리고 GL_TEXTURE_2D는 우리가 2D 텍스쳐를 사용한다는 말이고 img는 사용할 텍스쳐의 핸들, 그리고 마지막 0은 mipmap level 이다. 

이제 FBO 완성을 위해 마지막으로 해야 할 일은 FBO가 잘 만들어 졌는지 체크하는 일이다. 다음 함수로 한방에 체크하자.
GLenum status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);
모든 셋팅이 제대로 통과하였다면 GL_FRAMEBUFFER_COMPLETE_EXT를 리턴할 것이고 만약 에러가 있다면 다른 값을 리턴한다. 

4. 텍스쳐로 렌더링하기

이제 색상버퍼로 연결시켜 놓은 텍스쳐 (img)로 렌더링을 수행하는 방법을 알아볼 차례. 이건 상당히 쉬운데 걍 glBindFramebufferEXT() 함수만 호출하면 땡이다. 셋업이 조금 어려워서 그렇지 사용 자체는 굉장히 쉽다.

FBO로 렌더링 하기 위해서는 glBindFrameBufferEXT()를 호출하면 되고 렌더링을 멈추기 위해서는 같은 함수를 호출하되 인자값을 0으로 주면 된다.
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo); // FBO 바인딩
glPushAttrib(GL_VIEWPORT_BIT);                      
glViewport(0,0,width, height);                 // 여기서 부터는 텍스쳐에 렌더링한다.
// Render as normal here // output goes to the FBO and it’s attached buffers glPopAttrib(); glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); // 언바인딩. FBO에 렌더링하기 중지
자, FBO를 바인딩함으로써 차후 수행되는 모든 그리기 명령은 우리가 설정한 FBO의 색상 버퍼, 깊이 버퍼, 스텐실 버퍼에 적용된다. 그리고 glPushAttrib() 와 glPopAttrib() 함수를 이용해 이전의 3차원 매트릭스 값들을 신속히 복구 및 적용할 수 있게 했다. 

중요한 것은, 다시한번 강조하지만, glBindFramebufferEXT() 함수 하나로 간단하게 FBO로 렌더링을 할 수 있다는 점이다. 앞서 셋업에서 수행했던 몇몇 버퍼 바인딩 및 연결과정은 더이상 필요없다. 

5. 렌더링 된 FBO의 색상 버퍼를 텍스쳐로 사용하기

이제 우리의 궁극적인 목표에 가까워 졌다. 왜 우리는 FBO에 렌더링을 수행했던가? 바로 이 단계를 위해서다. FBO 색상 버퍼에 그려진 그림은 곧바로 텍스쳐로 사용 가능하다. 즉, 집안에 있는 TV 속의 화면을 우리는 방금 FBO를 이용해서 그려낸 것이다. 이제 TV 안의 화면은 그렸으니 집을 그릴때 만들어 놓은 TV 속의 화면을 텍스쳐로 입히자.

알다시피 img 핸들에 FBO의 색상 버퍼가 연결되어 있으므로 이 텍스쳐를 사용하기 위해서는 우선 바인딩 부터 한다.
glBindTexture(GL_TEXTURE_2D, img);
그리고 FBO 밉맵 자동 생성 함수인 glGenerateMipmapEXT() 함수를 호출하자.
glGenerateMipmapEXT(GL_TEXTURE_2D);
한가지 주의할 것은 만약 내가 임의의 밉맵 필터 (GL_LINEAR_MIPMAP_LINEAR와 같은)를 사용할 것이라면 FBO에 연결될 색상 버퍼 생성시 반드시 glGenerateMipmapEXT() 함수를 호출해서 가능한지 여부를 체크해야 한다. 

이 경우, 아래의 명령대로 FBO의 색상버퍼를 생성하면 된다.
glGenTextures(1, &img);
glBindTexture(GL_TEXTURE_2D, img);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8,  width, height, 0, GL_RGBA, 
             GL_UNSIGNED_BYTE, NULL);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glGenerateMipmapEXT(GL_TEXTURE_2D);
img를 바인딩 했다면 이제 할 일은 바인딩한 텍스쳐를 원하는 곳에 그려넣기인데 소스 코드를 보면서 설명해 보면...

void display(void)   
{
// FBO 바인딩 - FBO에 렌더링 하겠다고 알려줌
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo);
// 각종 화면 관련 속성 저장해 놓기 - FBO에 렌더링 끝나면 이전값으로 복귀하기 위해서
glPushAttrib(GL_VIEWPORT_BIT);
glViewport(0,0,width,height);

// 이제 부터 회전하는 작은 큐브를 FBO에 그린다.
glClearColor(0.0f, 0.0f, 0.0f, 0.5f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();

glTranslatef(0.0f,0.0f,-2.0f);
glRotatef(xrot,1.0f,0.0f,0.0f);
glRotatef(yrot,0.0f,1.0f,0.0f);

glBegin(GL_QUADS);
// Front Face
glColor4f(0.0f,1.0f,0.0f,1.0f);
glVertex3f(-0.5f, -0.5,  0.5);
glVertex3f( 0.5, -0.5,  0.5);
glVertex3f( 0.5,  0.5,  0.5);
glVertex3f(-0.5,  0.5,  0.5);
// Back Face
glColor4f(1.0f,0.0f,0.0f,1.0f);
glVertex3f(-0.5, -0.5, -0.5);
glVertex3f(-0.5,  0.5, -0.5);
glVertex3f( 0.5,  0.5, -0.5);
glVertex3f( 0.5, -0.5, -0.5);
// Top Face
glColor4f(0.0f,0.0f,1.0f,1.0f);
glVertex3f(-0.5,  0.5, -0.5);
glVertex3f(-0.5,  0.5,  0.5);
glVertex3f( 0.5,  0.5,  0.5);
glVertex3f( 0.5,  0.5, -0.5);
// Bottom Face
glColor4f(0.0f,1.0f,1.0f,1.0f);
glVertex3f(-0.5, -0.5, -0.5);
glVertex3f( 0.5, -0.5, -0.5);
glVertex3f( 0.5, -0.5,  0.5);
glVertex3f(-0.5, -0.5,  0.5);
// Right face
glColor4f(1.0f,1.0f,0.0f,1.0f);
glVertex3f( 0.5, -0.5, -0.5);
glVertex3f( 0.5,  0.5, -0.5);
glVertex3f( 0.5,  0.5,  0.5);
glVertex3f( 0.5, -0.5,  0.5);
// Left Face
glColor4f(1.0f,1.0f,1.0f,1.0f);
glVertex3f(-0.5, -0.5, -0.5);
glVertex3f(-0.5, -0.5,  0.5);
glVertex3f(-0.5,  0.5,  0.5);
glVertex3f(-0.5,  0.5, -0.5);
glEnd();

// 앞서 저장했던 화면 관련 값을 복구하고 FBO 사용 해제
glPopAttrib();
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);

        // 이제부터 그리는 것들은 스크린에 보여질 직접적인 것들
glClearColor(0.0f, 0.0f, 0.2f, 0.5f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
// 여기서 FBO에 그려진 회전하는 규브 (색상 버퍼)를 바인딩한다
glBindTexture(GL_TEXTURE_2D, img);
// 여기서는 밉맵 옵션이 사용되지 않지만 만약 당신이 밉맵을 사용한다면 아래의 주석을 제거하고 텍스쳐 생성시에 
// glGenerateMipmapEXT() 함수를 호출하라.
// glGenerateMipmapEXT(GL_TEXTURE_2D);
glEnable(GL_TEXTURE_2D);

glTranslatef(0.0f,0.0f,-2.0f);
glRotatef(-xrot,1.0f,0.0f,0.0f);
glRotatef(-yrot,0.0f,1.0f,0.0f);

glColor4f(1.0f,1.0f,1.0f,1.0f);

// 이제 FBO의 색상 버퍼를 텍스쳐로 사용하는 또다른 규브 그리기
glBegin(GL_QUADS);
// Front Face
glNormal3f( 0.0f, 0.0f, 1.0);
glTexCoord2f(0.0f, 1.0f); glVertex3f(-0.5f, -0.5,  0.5);
glTexCoord2f(1.0f, 1.0f); glVertex3f( 0.5, -0.5,  0.5);
glTexCoord2f(1.0f, 0.0f); glVertex3f( 0.5,  0.5,  0.5);
glTexCoord2f(0.0f, 0.0f); glVertex3f(-0.5,  0.5,  0.5);
// Back Face
glNormal3f( 0.0f, 0.0f,-1.0);
glTexCoord2f(1.0f, 0.0f); glVertex3f(-0.5, -0.5, -0.5);
glTexCoord2f(1.0f, 1.0f); glVertex3f(-0.5,  0.5, -0.5);
glTexCoord2f(0.0f, 1.0f); glVertex3f( 0.5,  0.5, -0.5);
glTexCoord2f(0.0f, 0.0f); glVertex3f( 0.5, -0.5, -0.5);
// Top Face
glNormal3f( 0.0f, 1.0, 0.0f);
glTexCoord2f(0.0f, 1.0f); glVertex3f(-0.5,  0.5, -0.5);
glTexCoord2f(0.0f, 0.0f); glVertex3f(-0.5,  0.5,  0.5);
glTexCoord2f(1.0f, 0.0f); glVertex3f( 0.5,  0.5,  0.5);
glTexCoord2f(1.0f, 1.0f); glVertex3f( 0.5,  0.5, -0.5);
// Bottom Face
glNormal3f( 0.0f,-1.0, 0.0f);
glTexCoord2f(1.0f, 1.0f); glVertex3f(-0.5, -0.5, -0.5);
glTexCoord2f(0.0f, 1.0f); glVertex3f( 0.5, -0.5, -0.5);
glTexCoord2f(0.0f, 0.0f); glVertex3f( 0.5, -0.5,  0.5);
glTexCoord2f(1.0f, 0.0f); glVertex3f(-0.5, -0.5,  0.5);
// Right face
glNormal3f( 1.0, 0.0f, 0.0f);
glTexCoord2f(1.0f, 0.0f); glVertex3f( 0.5, -0.5, -0.5);
glTexCoord2f(1.0f, 1.0f); glVertex3f( 0.5,  0.5, -0.5);
glTexCoord2f(0.0f, 1.0f); glVertex3f( 0.5,  0.5,  0.5);
glTexCoord2f(0.0f, 0.0f); glVertex3f( 0.5, -0.5,  0.5);
// Left Face
glNormal3f(-1.0, 0.0f, 0.0f);
glTexCoord2f(0.0f, 0.0f); glVertex3f(-0.5, -0.5, -0.5);
glTexCoord2f(1.0f, 0.0f); glVertex3f(-0.5, -0.5,  0.5);
glTexCoord2f(1.0f, 1.0f); glVertex3f(-0.5,  0.5,  0.5);
glTexCoord2f(0.0f, 1.0f); glVertex3f(-0.5,  0.5, -0.5);
glEnd();

glDisable(GL_TEXTURE_2D);
xrot+=xspeed;
yrot+=yspeed;

glutSwapBuffers ( );
// 버퍼 스왑 (출력하기)
}

위 프로그램을 돌려보면 일단 두개의 큐브가 나오는데 바깥쪽의 큰 큐브의 각 면은 FBO에 그려진 또 다른 회전하는 작은 큐브를 끊임없이 보여준다. (아래 그림 처럼)


6. 치우기

다 사용했다면 프로그램 종료전에 치우기를 수행해야 한다. (메모리 해제) 뭐 이것도 간단하다.
glDeleteFramebuffersEXT(1, &fbo);
위 명령은 fbo 를 삭제하라는 명령이고 앞서 만든 렌더버퍼를 날려보내기 위해서는 다음 명령을 수행한다.
glDeleteRenderbuffersEXT(1, &depthbuffer);
뭐 가운데 한 단어만 다를뿐 같은 명령이다.

마지막으로 글쓴이는 몇가지 당부를 하고 있는데, FBO를 생성했다 지웠다를 프로그램 실행중 반복하지 말라는 부탁과 렌더링된 FBO의 색상 버퍼를 glTexImage 함수를 이용해서 변경하거나 추가하는 행위를 피하는 것이 좋다고 한다. 이는 당연한 이야기로 이렇게 하면 Performance가 나빠질 것은 불을 보듯 뻔하다.

그리고 ATI를 사용하는 사람들에게는 위 예제가 제대로 동작하지 않을 수도 있단다. ATI 드라이버를 사용할때  FBO의 깊이 버퍼를 렌더버퍼로 연결하는 과정에 버그가 있다고 하는데 아마도 지금이 다 고쳐 졌겠지? 

소스 파일을 글에 첨부하였는데 Visual Studio 솔루션 파일이고 추가로 필요한 프로그램은 FreeGLUT이다. 이 FreeGLUT는 http://freeglut.sourceforge.net/에 가면 다운 받을수 있고 압축을 푼후 VisualStudio2008 폴더에 있는 프로젝트 파일을 열어서 컴파일 하게 되면 freeglut.lib와 freeglut.dll 파일이 생성된다. freeglut.dll은 윈도우즈 폴더에 System32 폴더안에 던져 넣어 버리고 lib 파일은 FBO_Example 프로젝트 폴더 안에 넣거나 FBO_Example/Release 폴더 및 FBO_Example/Debug 폴더에 던져 넣어버리면 문제 없이 컴파일 될것이다. 



posted by 대갈장군
2010. 3. 11. 00:56 프로그래밍
사용자 삽입 이미지
프로그램의 작성과 동작 원리는 위 그림처럼 간단한 편이다.

우선 소스 코드들 (.cpp, .c) 이 컴파일러에 의해서 컴파일 되면 바이너리 (기계어) 코드로 변경되어 .obj (오브젝트 코드)들로 저장이 된다.

그 후에 링커 (Linker)에 의해서 링크를 하게 되는데 이때 두가지 라이브러리를 사용할 수 있다. Static Library는 정적 라이브러리로써 실행 파일 (.exe)에 아예 포함되어 버리는 라이브러리다.

고로 .exe 자체가 소유하게 되므로 실행시에는 이 파일이 필요가 없다.

반대로 Import Library는 프로그램이 사용하는 특정 함수를 동적으로 가져다 쓰는 것으로써 실행을 위한 코드를 .exe 파일이 소유하는 것이 아니라 '어느 파일(DLL 파일들)에 니가 필요한 함수 뭐뭐가 있다더라' 라고 알려만 준다.

고로, .exe 파일을 실행할 때, 만약 내가 특정 함수를 DLL에서 불러와 사용했다면 반드시 해당 DLL과 .exe 파일을 연결 시켜 주어야 한다. 그렇지 않으면 .exe 는 실행 할 수가 없다는 에러 메세지를 띄운다.

.NET Framework는 이와는 조금 다르게 동작하기는 하지만 기본적으로 프로그램이 동작하는 원리는 비슷하다.

참조할 만한 관련글들
posted by 대갈장군
2010. 3. 11. 00:29 프로그래밍/C
예전에 다른 글에서 Visual Studio의 Code Generation 옵션에 대해서 이야기 한 적이 있다. 

위 글에서는 간단하게 옵션에 대한 설명만 했지만 왜 싱글 스레드에서 멀티 스레드로 바뀌었는지에 대해서는 충분히 설명하지 못했다. 그래서 이 글을 추가로 작성한다.

C 언어는 멀티 스레드 개념이 생기기 이전의 언어이므로 멀티 스레드 환경에서 발생하는 자원 경쟁 상태 (Race Condition)을 염두해 두지 않았다. 즉, 여러개의 스레드가 하나의 전역 변수를 공유하여 서로 바꾸려고 할때 발생하는 경쟁 상태를 말하는데 이것의 대표적인 예로 C 언어의 errno 전역 변수이다.

이 전역 변수는 호출한 함수의 결과를 저장하는 전역 변수인데 싱글 스레드의 경우에는 아무런 문제가 없으나 멀티 스레드의 경우 임의의 함수 호출 후 다른 스레드가 또 다른 함수를 호출하여 그 결과를 errno에 저장해 버릴 수 있기 때문에 100% 신뢰할 수 있는 결과를 가지지 않는다. 

그래서 Microsoft사에서 내놓은 해결책이 멀티 스레드를 위한 새로운 런타임 라이브러리 사용이었다. 멀티 스레드를 위한 런타임 라이브러리는 멀티 스레드에서도 안정적인 C 함수들을 호출하므로 100% 신뢰할 수 있다. 


내가 늘 불만인 것은 Visual Studio는 이렇게 중요한 옵션에 대한 설명이 너무나 부족하다는 것이다. 초보 프로그래머는 절대로 "왜 이걸 써야 하지?" 라는 궁극적인 질문에 대답을 할 수 없다.

MSDN에 설명이 되어 있다고 하지만 그 마저도 C 언어를 안다는 가정하에 설명하고 있어서 읽다가 짜증만 난다. 쉽게 쉽게 풀어서 설명해 주면 안되겠니? 응?

아무튼, 배포를 위한 프로그램을 개발한다면 거의 /MD를 사용하면 된다. D가 의미하는 것이 다이나믹 링크이므로 프로그램을 설치할 컴퓨터에 Visual Studio C++ 런타임 라이브러리만 잘 설치되어 있으면 프로그램은 무리 없이 돌아갈 것이다.

하지만 한가지 더 중요한 것은 이렇게 /MD 설정으로만 모든 것이 해결되는 것은 아니며 스레드 생성시 반드시 _beginthreadex() 함수를 사용해라는 것인데, 이 함수는 스레드 생성 전에 스레드를 위한 독립 저장 공간을 생성하여 자원 경쟁을 예방한다. 이런 독립 공간을 스레드 지역 저장소라하며 원어로를 Thread Local Storage 라 부른다. 

그리고 생성을 저 함수로 했다면 스레드를 죽일때에는 _endthreadex()를 사용해야 한다. 

결론적으로,
  • C 언어는 싱글 스레드를 근간으로 작성되었으므로 멀티 스레드의 자원 경쟁 문제를 유발할 수 있다.
  • 그런 발생 가능한 문제 제거를 위해서는 멀티 스레드 런타임 라이브러리 옵션 (/MD)를 사용하면 된다.
  • 완벽한 문제 발생 제거를 위해서는 _beginthreadex()_endthreadex() 함수로 스레드를 생성하고 해제하라.


'프로그래밍 > C' 카테고리의 다른 글

C Language Constructors and Destructors with GCC  (0) 2015.02.12
C Standard Library  (0) 2010.09.25
호출 규약  (0) 2009.07.28
posted by 대갈장군
2010. 3. 6. 03:53 프로그래밍
왜 언어코드 체계가 이렇게 짜증이 나는지 원... 에휴...

1. SBCS (Single Byte Character Set) : ASCII 문자셋과 ANSI 문자셋 처럼 한바이트로 문자를 표현하는 코드. 고로 최대 256개 까지 가능

문제는 꼴랑 256개 밖에 지원이 안되는 문제점. => 우리나라나 중국은 어쩌라고?
그래서 나온것이 DBCS

2. DBCS (Double Byte Character Set) : 원래 이 DBCS의 의미는 2바이트로 문자를 표현한다는 의미. 하지만 영문과 기호는 8비트로 표현하고 한글은 16비트로 표현하기 때문에 2바이트의 한글과 1바이트의 영문자가 공존 (지 맘대로 섞여 있음). 그래서 다른 말로 MBCS (Multi Byte Character Set) 이라고도 한다.

MBCS의 문제는 글자가 몇개인지 셀때 혹은 바로 앞문자가 뭔지 찾을때 일일이 앞에서 부터 차례대로 검색해야만 알수 있다는 문제점 있음. 왜냐고? 같은 길이를 가지는 문자열이라도 1바이트 영문, 기호랑 2바이트 한글이 몇개씩 들어가 있는지 알수 없으니까.

그래서 등장 한 것이 바로
유니코드다....

3. 유니코드 : 전세계 문자를 표현하는 코드 체계. 16비트로 표현하여 최대 65536개 문자 표현.

이 유니코드는 각 문자가 2바이트씩 딱딱 차지하므로 MBCS에서와 같은 문제는 없다. 하지만 영문자의 경우 공간 낭비가 발생한다.


유니코드의 장점
모든 글자가 같은 크기를 가지므로 글자 수나 검색이 한결 용이함

유니코드의 단점
알파벳은 1바이트만 있어도 충분히 표현가능한데 2바이트를 사용하여 공간 낭비
한글의 고어나 중국의 고어를 넣을 만한 충분한 여유 공간이 없다

고로 위의 유니코드의 문제점 해결이 필요해 졌으니, 그 대안으로 등장한 것이 UTF 다.

4. UTF (UCS Transformation format) : UTF-1, UTF-7, UTF-8, UTF-16, UTF-32등 유니코드 바이트 스트림에 따른 서로 다른 인코딩 방식을 총칭.

UTF는 유니코드를 확장하기 위한 인코딩 방식인데
UTF-16의 경우는, 우선 일반적으로 유니코드 2바이트 형식으로 표현가능한 글자는 그냥 2바이트로 표기하고 만약 한자 고어, 한국 고어 같은 것이 필요하면 4바이트로 확장하여 사용하는 것인데 한마디로 가변 코드 체계다.

이것도 MBCS처럼 2바이트 유니코드와 4바이트 코드가 공존하므로 약간 다루기 까다롭다는 문제가 있구만..

UTF-8의 경우는 또 조금 다른데 이것은 미국이나 유럽등 알파벳을 사용하는 나라를 위해서 사용하는 인코딩 기법이다.

알다시피 알파벳은 1바이트면 충분하다. 그래서 UTF-8의 경우 가변 길이를 가지는 코드체계로써 만약 알파벳이면 1바이트 한글, 중국어면 3, 4바이트를 이용하는 효율적인 방법이다.

고로 만약
문서에 한글(고어 말고)이 많으면 UTF-16이 유리할 것이고 만약 알파벳이 많으면 UTF-8이 유리하다.


헌데 UTF-8이 훨씬도 똑똑한데 왜 그러냐면,
1. 문자 코드 길이 계산이 쉽다. 상위 첫 비트가 중복되는 경우가 없으므로 (고유 태그) 길이 계산이 용이하다. (길이 계산 용이)
2. 가변 길이의 코드도 앞과 뒤가 고유 태그를 가지므로 현재 바이트의 헤더 부분만 보고도 이 바이트가 문자의 앞인지 아니면 중간인지 알수 있다. (글자 검색 용이)
3. 문자열 검색이 용이하다. 왜? 마찬가지로 중복 코드가 없으므로.

추가로 중요한 것 한가지는 윈도우 95랑 98에서는 유니코드 지원하지 않는다 (기본적으로) 물론 업뎃하면 된다고는 하지만 완벽 지원은 안된단다. (지금도 그런지는 모름)

프로그램짜면서 사용해야 하는 실제 상황을 보자꾸나...


#define UNICODE
#include <windows.h>

UNICODE를 windows.h 전에 선언해주면 그 프로젝트는 유니코드로 컴파일 된다.

wchar_t *lpszClass = L"UniTest1";

유니코드로 문자열을 선언하기 위해서는 위 명령처럼 wchar_t 타입을 사용해야 하고 L을 " " 문자열 앞에 붙여야 한다. 

여기서 wchar_t는 사실 unsigned short (16 비트)이고 L은 뒤에 있는 문자열을 2바이트 유니코드로 변환하여 저장하라는 매크로다. 이 L이 없으면 컴파일러는 기본적으로 ANSI (1바이트 문자열 형식)로 저장하려 한다. (에러 뜰거임)

그러면 프로그램짤때 유니코드 쓰냐 안쓰냐에 따라서 선언문이 바뀌어야 하느냐? 만약 YES라면 얼마나 불편하겠는가. 그래서 등장한 놈이 TCHAR인데...

typedef wchar_t WCHAR;
typedef char CHAR;

#ifdef UNICODE
typedef WCHAR TCHAR;
#else
typedef char TCHAR;
#endif

위의 전처리를 통해서 ANSI든 유니코드든간에 TCHAR을 사용하면 UNICODE의 선언 여부에 따라 자동으로 컴파일러가 골라쓰게 된다. 즉, UNICODE가 선언되어 있으면 TCHAR이 WCHAR이고 결국 이건 wchar_t이며 이건 결국 unsigned short이다. 만약 UNICODE 선언이 없다면 TCHAR은 걍 char이다. 

마찬가지로 문자열에 대해서는

typedef WCHAR *PWCHAR, *LPWSTR, *PWSTR;                   // 유니코드 사용시 문자열 포인터 타입
typedef CONST WCHAR *LPCWCH, *LPCWSTR, *PCWSTR;   // 유니코드 사용시 상수형 문자열 포인터 타입
typedef CHAR *PCHAR, *LPSTR, *PSTR;                             // 유니코드 사용 안할때 문자열 포인터 타입
typedef CONST CHAR *LPCCH, *LPCSTR, *PCSTR;             // 유니코드 사용 안할때 상수형 문자열 포인터 타입

#ifdef UNICODE                                                 // 유니코드 사용시는 PTSTR = LPWSTR, LPCTSTR = LPCWSTR
typedef LPWSTR 
PTSTR, LPTSTR;
typedef LPCWSTR 
LPCTSTR;
#else                                                               // 유니코드 사용안할때는 PTSTR = LPSTR, LPCTSTR = LPCSTR
typedef LPSTR 
PTSTR, LPTSTR;
typedef LPCSTR 
LPCTSTR;
#endif

인데 졸라 복잡해 보이지만 간단하다. 문자열 포인터나 상수형 문자열 포인터를 써야 한다면 간단하게 걍 PTSTR, LPTSTR, LPCTSTR등을 쓰면 된다. 만약 내가 #define UNICODE를 시작전에 선언했다면 프로그램은 알아서 WCHAR 을 사용할 것이고 만약 유니코드 매크로를 선언하지 않았다면 알아서 CHAR을 사용한다. 위의 선언에서 잘 보면 W가 있고 없고의 차이인데 이것이로 WCHAR이냐 아니면 CHAR이냐의 차이다.

고로 결론적으로 유니코드도 되고 ANSI도 되는 코드를 짜려면 문자 선언시는 TCHAR, 문자열 선언시는 LPTSTR을 써라는 말이다.

문자열 상수에 대한 매크로가 존재하는데 바로 TEXT("     ") 이다. 이놈을 사용하면 마찬가지로 매크로에서 정의된 방법대로 유니코드냐 아니냐에 따라서 알아서 저장해 준다. 만약 UNICODE 매크로가 선언되어 있다면 TEXT()는 알아서 문자열 앞에 L을 붙여주고 (이 L이 유니코드로 문자열을 변경하라는 매크로) 만약 UNICODE 매크로가 선언 되지 않았다면 걍 ANSI 문자열로 문자열을 생성한다. 고로 사용자는 단순하게 #define UNICODE를 프로그램 시작부분에 쓰느냐 안쓰느냐에 따라서 간단하게 유니코드와 ANSI 코드 사이를 왔다 갔다리 할 수 있는 거다

사실 알고보면 윈도우가 제공하는 각종 API 함수들중 문자열을 받거나 리턴하는 대부분 UNICODE 타입과 ANSI 타입을 지원하는 두 종류를 가진다. 예를 들어 TextOut 함수를 보면 실제로는 TextOutW와 TextOutA라는 두 함수로 구성되어 있고 UNICODE의 선언 여부에 따라 적절한 함수를 호출한다. 여기서 W는 Wide라는 의미로 Unicode를  의미하고 A는 ANSI를 의미한다.


프로그램을 작성할 때는 윈도우가 제공하는 API 뿐만 아니라 오래된 C 함수 및 C++함수들을 사용해야 하는데 대표적인 것이 문자열의 길이를 구하는 함수인 strlen()이다. 대략 10년 전쯤 처음 컴퓨터 프로그래밍 수업시간에 들었던 기억이 난다.. ㅡ.ㅡ;

아무튼, 이 strlen() 함수는 ANSI 코드를 위한 함수이지 유니코드를 위한 함수가 아니다. 유니코드를 위한 문자열 길이 구하는 함수는 wcslen() 이라는 놈이다. 하지만 걱정할 필요가 없는것이 위에서 해왔던 것 처럼 이 둘 사이를 쉽게 왔다 갔다 할 수 있는 일반형 함수가 존재한다. 

tchar.h에 보면 UNICODE의 사용여부에 따라 알아서 strlen() 또는 wcslen() 함수를 사용하도록 해주는 일반형 함수가 정의되어 있는데 대표적인 예로 _tcslen, _tcscpy, _tcscmp, _tcsicmp 등이다.

#ifdef _UNICODE
typedef _tcslen wcslen;
#else
typedef _tcslen strlen;
#endif

위 매크로 선언에 의해서 알아서 wcslen과 strlen 함수를 _UNICODE 매크로 선언 여부에 따라 적절히 선택한다.

참고로 코드를 보면 _UNICODE와 UNICODE 두가지가 공존하는데 이것은 같은 의미의 매크로로써 하나는 컴파일러가 언어차원에서 정의한 것이고 나머지 하나는 운영체제가 정의하는 거란다. 젠장할...

아무튼 결론적으로보면 사용자는 프로그램 내부에 TCHAR, _tcslen, TEXT(" ") 와 같은 UNICODE 선언 여부에 따라 적절한 함수를 선택하는 매크로 함수들을 사용하면 손쉽게 UNICODE 용으로 컴파일 할수도 있고 혹은 ANSI 용으로 컴파일 할 수도 있다는 점이다. 


Visual Studio 의 프로젝터 Properties 를 클릭해 보면 General 속성에서 Character Set에 유니코드와 MBCS 둘 중 하나를 선택할 수 있다. 여기서 유니코드를 선택하면 Visual Studio가 알아서 #define UNICODE를 코드에 삽입한다. (얼마나 편한가..)


만약 프로그램을 짤 때 일반화 함수들을 안쓰고 char을 써놓고 UNICODE로 컴파일 하라는 명령을 주게 되면 위 그림처럼 무수히 많은 에러들을 보게 될 것이다. 이것이 일반적으로 가장 빈번히 발생하는 유니코드 관련 에러이다. 문제는 다음과 같은 경우다. 

내가 프로그램을 만드는데 어느 유명 사이트에서 대단한 프로그래머가 만든 아주 유용한 소스 코드를 받았다. 헌데 그 프로그래머는 너무나도 유니코드를 사랑하셔서 모든 함수를 일반화 함수가 아닌 유니코드 함수로 직접 불러다 사용했다. 그런데 내가 만든 프로그램에서는 죄다 MBCS를 사용한다. (char, char * 같은 형태) 

둘을 하나의 프로젝트에 묶어서 컴파일 하려했다... 에러가 200개 뜬다..... ㅡ.ㅡ; 헐... 

이런 경험을 해본 사람이 많으리라 믿는다. 아무튼, 말하고자 하는 바는 프로젝트를 만들때 유니코드와 MBCS가 동시에 존재할 수 없다는 것이다. 그래서 일반화 함수를 사용하는 것이 현명한 방법이다.

TCHAR
TCHAR과 PTSTR 그리고 TEXT() 매크로 함수를 범용성을 위해서 꼭 사용해라... 이말이다.

posted by 대갈장군
2010. 3. 6. 01:34 카테고리 없음

요즘 유일하게 보는 드라마 추노... 모두들 대단하지만 그중에서도 성동일, 아니지 아니지... '천지호'.

나는 성동일을 본것이 아니라 수백년전 살다 죽은 '천지호'를 보았다. 

진짜 배우라면 주어진 배역을 '연기'하지 않는다. 나는 천지호의 어디에서도 성동일을 찾아 낼 수 없었다.

자아를 비우고 새로운 인생을 살아가는 진정한 연기자... 

다음 작품에서 어떤 역활을 하게 되더라도 마찬가지로 나는 '성동일'을 볼 수 없으리라 생각한다.

아마도 나는 영원히 진짜 성동일을 보지 못할 것이다... 하하.. 이것이 진짜 연기자가 아닌가?

언니가 최고입니다요.. 





posted by 대갈장군
2010. 3. 5. 05:25 프로그래밍

Managed, Unmanaged, Native: What Kind of Code Is This?

http://www.developer.com/net/cplus/article.php/2197621/Managed-Unmanaged-Native-What-Kind-of-Code-Is-This.htm

With the release of Visual Studio .NET 2003 (formerly known as Everett) on April 24th, many developers are now willing to consider using the new technology known as managed code. But especially for C++ developers, it can be a bit confusing. That's because C++, as I pointed out in my first column here, is special.

1. What Is Managed Code? – Managed code?

Managed Code is what Visual Basic .NET and C# compilers create. It compiles to Intermediate Language (IL), not to machine code that could run directly on your computer. The IL is kept in a file called an assembly, along with metadata that describes the classes, methods, and attributes (such as security requirements) of the code you've created. This assembly is the one-stop-shopping unit of deployment in the .NET world. You copy it to another server to deploy the assembly there—and often that copying is the only step required in the deployment.

Manage code 무엇인가? Managed code Visual Basic .NET C# 컴파일러가 만들어내는 것을 말한다. 컴파일러들은 우선 IL(Intermediate Language – 중간 언어) 라고 불리는 코드를 만들어 낸다. IL 컴파일을 수행한 당신의 컴퓨터에서 바로 실행되기 위한 machine code (기계 언어) 아니라는 알아야 한다. 이렇게 생성된 IL 어셈블리라고 불리는 파일에 저장되고 이때 메타 데이터도 함께 저장하는데 메타 데이터들은 내가 컴파일한 코드의 클래스, 함수 그리고 변수 속성 (보안 관련 속성도 포함)등을 가진다. 이제 어셈블리 파일은 .NET 세계에서 배달 완료된 모든 내용이 포장된 일종의 상품이다. 당신은 이것을 다른 서버에 복사하여 deploy (배포)하기만 하면 된다.

Managed code runs in the Common Language Runtime. The runtime offers a wide variety of services to your running code. In the usual course of events, it first loads and verifies the assembly to make sure the IL is okay. Then, just in time, as methods are called, the runtime arranges for them to be compiled to machine code suitable for the machine the assembly is running on, and caches this machine code to be used the next time the method is called. (This is called Just In Time, or JIT compiling, or often just Jitting.)

Managed code 이른바 Common Language Runtime (줄여서 CLR) 환경에서 동작한다. CLR 내가 작성한 코드가 실행될 다양한 형태의 서비스를 지원한다. 여기서 주의 깊게 봐야 하는 단어는 실행될 이다. 일반적인 경우, CLR 우선 어셈블리 파일을 읽은 다음 IL 괜찮은지부터 확인한다. 그리고 함수가 호출되는 순간 CLR 현재 프로그램이 돌아가는 컴퓨터의 환경에 적합한 기계 언어를 만들어 낸다. 그리고 함수가 다음 번에 호출되면 바로 사용될 있게 하기 위해서 만들어진 기계언어 코드를 cache (캐시) 저장해 둔다. 이것이 바로 Just In Time 혹은 줄여서 JIT 컴파일링 이라 불리는 기법이다. 여기서 중요한 것은 현재 프로그램이 돌아가는 컴퓨터 환경에 적합한 기계 언어 생성이다. 이것이 바로 CLR 강점이다.

As the assembly runs, the runtime continues to provide services such as security, memory management, threading, and the like. The application is managed by the runtime.

어셈블리가 실행되는 동안 CLR 보안, 메모리 관리, 스레딩에 관련된 서비스 일체를 제공하고 이런 프로그램을 우리는 ‘Runtime 의해 Managed (관리되는) 프로그램이다 라고 한다.

Visual Basic .NET and C# can produce only managed code. If you're working with those applications, you are making managed code. Visual C++ .NET can produce managed code if you like: When you create a project, select one of the application types whose name starts with .Managed., such as .Managed C++ application..

Visual Basic .NET C# 오직 managed 코드만 생성가능 하다. 만약 당신이 프로그램들을 사용한다면 당신은 managed code 생성해 내고 있는 것이다. 반면 Visual C++ .NET 내가 원하면 managed code 생성할 수도 있고 아니면 unmanaged code 생성할 수도 있다. 이것은 Visual Studio 프로젝트 속성에서 변경 가능하다.

2. What Is Unmanaged Code? – Unmanaged code ?

Unmanaged code is what you use to make before Visual Studio .NET 2002 was released. Visual Basic 6, Visual C++ 6, heck, even that 15-year old C compiler you may still have kicking around on your hard drive all produced unmanaged code. It compiled directly to machine code that ran on the machine where you compiled it—and on other machines as long as they had the same chip, or nearly the same. It didn't get services such as security or memory management from an invisible runtime; it got them from the operating system. And importantly, it got them from the operating system explicitly, by asking for them, usually by calling an API provided in the Windows SDK. More recent unmanaged applications got operating system services through COM calls.

Unmanaged code Visual Studio .NET 2002 나오기 전에 만든 코드를 말한다. , Visual Basic 6, Visual C++ 6 그리고 15 이상된 C 컴파일러 또한 Unmanaged code 생성해 낸다. 이것들은 내가 컴파일을 수행하는 바로 컴퓨터 적합한 기계 코드를 생성 낸다. (Managed code 처럼 IL 생성 과정이 없다.) 고로 이렇게 생성해낸 기계 언어는 같은 하드웨어 구성을 가지지 다른 컴퓨터에서는 돌아갈지 모르나 다른 구성을 가지는 컴퓨터에서는 당연히 실행 불가다. 또한 이렇게 생성된 unmanaged code 보안 메모리 관리 서비스를 실행시에 Runtime으로부터 받을 수가 없다. 대신 OS(Windows)로부터 받는다. 여기서 중요한 점은 OS로부터 서비스를 받기는 하지만 이것은 Windows SDK 제공하는 특정 함수 (API) 호출함으로써 가능하다. 근래의 Unmanaged code 운영체제의 서비스를 COM call 의해 제공받는다라고 했다. 글이 2004 글이므로 COM 근래의 언어라고 표현한 것이 이해가 간다. 아무튼, 요약하자면 managed code 경우 runtime 알아서 메모리 관리를 해주는데 반해 (Garbage collection) unmanaged code 경우, 사용자에게 책임이 있다는 점이다. 다만 COM 같이 발전된 형태의 라이브러리들은 스스로 객체의 파괴를 관리하는 능동성을 가지고는 있다.

Unlike the other Microsoft languages in Visual Studio, Visual C++ can create unmanaged applications. When you create a project and select an application type whose name starts with MFC, ATL, or Win32, you're creating an unmanaged application.

다른 Microsoft 언어들과는 다르게 Visual C++ unmanaged 프로그램을 만들 있다. MFC, ATL 혹은 Win32 생성되는 프로젝트들은 모두 unmanaged application이다.

This can lead to some confusion: When you create a .Managed C++ application., the build product is an assembly of IL with an .exe extension. When you create an MFC application, the build product is a Windows executable file of native code, also with an .exe extension. The internal layout of the two files is utterly different. You can use the Intermediate Language Disassembler, ildasm, to look inside an assembly and see the metadata and IL. Try pointing ildasm at an unmanaged exe and you'll be told it has no valid CLR (Common Language Runtime) header and can't be disassembled—Same extension, completely different files.

당연히 Visual C++ Managed application 만들 수도 있다. 이때 .exe 확장자를 가지는 IL 어셈블리가 만들어진다. 만약 당신이 MFC 프로그램(unmanaged code) 만든다면 이것은 똑같은 확장자인 .exe 가지는 native code 생성해 것이다. 여기서 혼란이 수도 있다. 같은 .exe 확장자를 가지니까 뭐가 뭔지 모를 있다. 둘을 구분하기 위해서는 Intermediate Language Disassembler, 줄여서 ildasm이라 불리는 놈으로 어셈블리와 메타 데이터를 들여다 보면 된다. Unmanaged code ildasm으로 보려고 하면 유효한 CLR 헤더가 없다고 에러 메시지를 출력할 것이란다. , 개의 .exe 파일은 완전히 다른 형태의 파일이다.

3. What about Native Code? – Native code?

The phrase native code is used in two contexts. Many people use it as a synonym for unmanaged code: code built with an older tool, or deliberately chosen in Visual C++, that does not run in the runtime, but instead runs natively on the machine. This might be a complete application, or it might be a COM component or DLL that is being called from managed code using COM Interop or PInvoke, two powerful tools that make sure you can use your old code when you move to the new world. I prefer to say .unmanaged code. for this meaning, because it emphasizes that the code does not get the services of the runtime. For example, Code Access Security in managed code prevents code loaded from another server from performing certain destructive actions. If your application calls out to unmanaged code loaded from another server, you won't get that protection.

Native code 가지의 의미를 가지고 있다. 우선 Native code 함은 Unmanaged code 동의어로 사용된다. 옛날 버전의 컴파일러로 컴파일 되었거나 Visual C++에서 일부러 선택한 컴파일러에 의해 해당 컴퓨터에 적합한 기계언어의 생성을 말하며 이는 런타임시 (실행되는 순간) CLR 의해 아무런 서비스를 받을 없다는 것을 의미한다. 이런 Native code(Unmanaged code) 하나의 완전한 프로그램 수도 있고, COM 컴포넌트 수도 있고, 혹은 Managed code내부에서 호출된 DLL 수도 있다. 하지만 이런 경우 ‘Unmanaged code’라고 하는 것이 합당한데 왜냐면 Unmanaged code라는 것이 실행시에 Runtime으로부터 서비스를 받을수 없다는 것을 분명히 강조하기 때문이란다. 예를 들어 설명한 것이 서로 다른 컴퓨터에서 실행되는 프로그램이 Managed code 경우 보안 관련 서비스를 실행시 (Runtime) 받을수 있으나 Unmanaged code 경우 그럴 없다는 .

The other use of the phrase native code is to describe the output of the JIT compiler, the machine code that actually runs in the runtime. It's managed, but it's not IL, it's machine code. As a result, don't just assume that native = unmanaged.

다른 의미의 Native code JIT 컴파일로부터 생성된 코드를 말한단다. JIT Managed code에만 사용되므로 이것은 분명 Managed code이지만 IL 코드가 아니고 해당 컴퓨터에 적합한 기계언어이다. 고로 Native라는 단어를 무조건적으로 Unmanaged라고 생각하지 말라.

Does Managed Code Mean Managed Data? – Managed code Managed Data 의미하나?

Again with Visual Basic and C#, life is simple because you get no choice. When you declare a class in those languages, instances of it are created on the managed heap, and the garbage collector takes care of lifetime issues. But in Visual C++, you get a choice. Even when you're creating a managed application, you decide class by class whether it's a managed type or an unmanaged type. This is an unmanaged type:

Visual Basic이나 C# 사용한다면 모든 것이 Managed code이므로 오히려 간단하다. 고로 내가 만약 언어들을 사용해서 프로그램을 작성한다면, 클래스를 하나 선언하고 객체화를 했을 객체는 Managed Heap 생성되고 Garbage Collector 알아서 메모리를 수거해 간다. 하지만 내가 만약 Visual C++ 사용한다면 나는 각각의 클래스를 선언할 마다 선택을 수가 있게 된다. 이른바 클래스가 Managed type인가 아니면 Unmanaged type인가를 다음과 같이 결정한다. 우선 Unmanaged type 경우 다음과 같이 선언한다.

class Foo

{

private:

   int x;

public:

    Foo(): x(0){}

    Foo(int xx): x(xx) {}

};

 

This is a managed type:

그리고 Managed type 경우 다음과 같다.

__gc class Bar

{

private:

   int x;

public:

    Bar(): x(0){}

    Bar(int xx): x(xx) {}

};

The only difference is the __gc keyword on the definition of Bar. But it makes a huge difference.

차이점이라고는 클래스 선언 앞부분에 __gc 붙냐 안붙냐 차이 밖에 없다. 하지만 차이는 엄청나다.

Managed types are garbage collected. They must be created with new, never on the stack. So this line is fine:

Foo f;

Managed type garbage collector 의해 자동으로 메모리가 수거 된다. 고로 놈들은 반드시 new 이용해서 생성해야 하며 절대로 stack 생성될 없다. 고로 위에 Unmanaged type으로 생성한 Foo 클래스의 경우 명령처럼 스택에 선언할 있지만,

But this line is not allowed:

Bar b;

Managed type으로 선언한 Bar 클래스의 경우 Foo 클래스와 같은 방식으로 스택에 선언 수가 없다.

If I do create an instance of Foo on the heap, I must remember to clean it up:

Foo* pf = new Foo(2);

// . . .

delete pf;

The C++ compiler actually uses two heaps, a managed an unmanaged one, and uses operator overloading on new to decide where to allocate memory when you create an instance with new.

만약 내가 Foo 클래스를 힙에 선언한다면 (다른 말로 포인터 생성하여 new 객체화 경우), 반드시 메모리 삭제를 delete 통해서 파괴해야 한다. C++ 컴파일러는 실제로 두개의 힙을 사용하는데 각각은 managed unmanaged 위한 것이다. new 라는 키워드는 오버로딩을 통해서 managed unmanaged 중에 하나를 선택하여 메모리 할당한다.

If I create an instance of Bar on the heap, I can ignore it. The garbage collector will clean it up some after it becomes clear that no one is using it (no more pointers to it are in scope).

There are restrictions on managed types: They can't use multiple inheritance or inherit from unmanaged types, they can't allow private access with the friend keyword, and they can't implement a copy constructor, to name a few. So, you might not want your classes to be managed classes. But that doesn't mean you don't want your code to be managed code. In Visual C++, you get the choice.

반대로 만약 내가 Bar 클래스를 힙에 생성하면 나는 간단히 신경 끄면 그만이다. ? Garbage collector 알아서 메모리를 수거할 테니

, managed type에는 몇가지 제약이 있으니 바로 다중 상속이 안되며 unmanaged type으로 부터의 상속이 안된다는 제약이다. 또한 private 속성에 대한 접근이 friend 키워드를 통해서도 안되고, 복사 생성자를 구현 수가 없단다. 요런 저런 제약을 보아하니 아마도 당신은 managed class 만들고 싶지 않을 수도 있다. 하지만 그것이 당신의 프로그램 전체가 managed code 아니길 바란다는 것은 아닐 것이다 (managed unmanaged 섞어 써도 된다는 ). 암튼, C++에서는 프로그래머가 선택할 있다.

결론만 말하자면,

1. Managed code는 CLR에 의해 제공되는 실행시의 각종 서비스 (보안 및 메모리 관련)를 받을 수 있는 것을 말한다.
2. Managed code는 CLR에 의해 해당 컴퓨터에 적합한 기계 언어를 능동적으로 생성하여 범용성을 높였다.
3. Unmanaged code는 실행시의 CLR이 제공하는 서비스를 제공받을 수 없다.
4. Unmanaged code는 컴파일할때 컴퓨터의 환경에 적합한 기계어만 생성하므로 범용성이 매우 낮다.
5. Native code는 '기계 언어'라고 보는 것이 적합하며 Native code를 Unmanaged code라고 생각하면 안된다.
6. Visual C++는 Managed Type과 Unmanaged Type의 클래스를 생성할 수 있다.
7. Managed Type 클래스는 힙에 생성되면 자동으로 메모리 수거가 이루어 지는 반면 Unmanaged Type 클래스는 사용자가 수거해야 한다. 
8. __gc를 클래스 선언시 사용하면 Managed Type 클래스를 선언 할 수 있다.

About the Author

Kate Gregory is a founding partner of Gregory Consulting Limited (www.gregcons.com). In January 2002, she was appointed MSDN Regional Director for Toronto, Canada. Her experience with C++ stretches back to before Visual C++ existed. She is a well-known speaker and lecturer at colleges and Microsoft events on subjects such as .NET, Visual Studio, XML, UML, C++, Java, and the Internet. Kate and her colleagues at Gregory Consulting specialize in combining software develoment with Web site development to create active sites. They build quality custom and off-the-shelf software components for Web pages and other applications. Kate is the author of numerous books for Que, including Special Edition Using Visual C++ .NET.

 

posted by 대갈장군
2010. 2. 26. 01:23 World Of Warcraft




'World Of Warcraft' 카테고리의 다른 글

Paladin  (0) 2009.03.13
posted by 대갈장군
2010. 2. 24. 06:01 OpenGL
Convex Hull은 볼록 껍데기로써 임의의 점으로 구성된 다각형에서 두 점을 연결했을때 이 두 점이 이루는 직선이 다각형의 외곽선을 교차하지 않는 것을 의미한다. 바로 다음 그림처럼..


반대로 임의의 두점이 외곽선을 관통하게 되면 (아래 그림처럼) 이것을 오목 껍데기 혹은 Concave Hull 이라고 한다. 중요한 것은 OpenGL에서는 오직 Convex Hull 로만 폴리곤을 그릴수 있다는 것... :) 


posted by 대갈장군
2010. 2. 24. 03:30 OpenGL
OpenGL에서 블렌딩은 예상외로 조금 복잡하다. 일단 함수의 인자부터 헷갈린다. glBlendFunc() 함수가 입력으로 들어오는 화소와 프레임 버퍼에 이미 그려져 있는 화소의 블렌딩 비율을 지정하는 놈인데 이 놈의 인자들을 살펴보면, 첫번째 인자가 source이고 두번째가 destination 이다. 

이거 마치 source가 원래 그려져 있던 화소 같이 들리지만 그 반대다. source인 첫번째 인자가 들어오는 화소고 두번째 인자인 destination이 목적지인 그려질 위치에 있던 화소를 말한다.

아래에 두 가지 대표적인 블렌딩의 예를 그림으로 표현했는데 우선 첫번째 인자가 GL_SRC_ALPHA이고 두번째 인자가 GL_ONE_MINUS_SRC_ALPHA 이면 입력으로 들어오는 화소의 알파값이 입력 화소의 블렌딩 비율이되고 1에서 입력으로 들어오는 화소의 알파값을 뺀 나머지 값이 원래 버퍼에 들어있던 화소의 블렌딩 비율이 되게 하라는 말이다.

고로 소스로 들어온 사각형의 알파값이 아래 그림 첫번째 처럼 50% 였다면, 1 - 0.5를 뺀 값인 0.5가 원래 있던 화소들인 빨강색 삼각형에 적용되어 둘이 합쳐져 오른쪽의 그림처럼 스크린에 나타난다.

만약 입력으로 들어오는 사각형의 알파값이 20%라면 삼각형은 80% (100 - 20)로 블렌딩 되어 그림 내부의 아래 그림 처럼 블렌딩 된다.


것을 코드로 표현하자면 아래와 같다. 우선 블렌딩을 활성화하고 사각형을 그리는데 블렌딩 옵션을 입력으로 들어오는 화소만 그려지라는 의미의 GL_ONE, GL_ZERO 로 주고 삼각형을 그린다. 그리고나서 블렌딩 옵션을 바꿔서 사각형을 그리면 된다.


  glEnable(GL_BLEND);  // 블렌딩활성화

  glBlendFunc(GL_ONE, GL_ZERO);  // 소스(Incoming) 만그리기

 

  glBegin(GL_TRIANGLES);  // Drawing Using Triangles

  glColor4f(1.0f, 0.0f, 0.0f, 1.0f);

  glVertex4f( 0.0f, 1.2f, 0.0f, 1.0f); 

  glVertex4f(-1.2f,-1.0f, 0.0f, 1.0f); 

  glVertex4f( 1.2f,-1.0f, 0.0f, 1.0f); 

  glEnd(); 


  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);  

 

  glBegin(GL_QUADS);  // Draw A Quad

  glColor4f(0.0f, 0.0f, 1.0f, 0.5f);

  glVertex4f(-1.0f, 1.0f, 0.0f, 1.0f); 

  glVertex4f( 1.0f, 1.0f, 0.0f, 1.0f); 

  glVertex4f( 1.0f,-1.0f, 0.0f, 1.0f); 

  glVertex4f(-1.0f,-1.0f, 0.0f, 1.0f); 

  glEnd(); 

 

또 다른 예로는 3개의 물체를 균일한 값으로 블렌딩 하는 것인데 여기서는 3개의 물체 (원, 사각형, 삼각형)가 나온다. 이것도 위와 유사한 방법이지만 glBlendFunc() 함수의 인자를 GL_SRC_ALPHA와 GL_ONE으로 준다. 소스로 들어오는 화소의 블렌딩은 들어오는 알파값을 그대로 사용하되 먼저 그려져 있던 화소는 GL_ONE을 줌으로써 가지고 있던 알파값을 그대로 유지하게 한다. 결국 이 의미는 '중첩'시키라는 의미다. 고로 33%씩 세번 중첩 하면 100% 하얀 부분이 3개의 물체가 겹치는 부분에 나타나야 한다. 근데 아래 그림은 좀 약하다... 파워 포인트로 작업한지라 제대로 중첩이 안되고 있다.. -_-  

그래서 프로그램을 돌려 나온 결과를 스크린 캡쳐하여 첨부하였다. 아래 그림을 보면 아... 세개의 도형이 겹치는 부분은 100% 흰색이구나 하는 느낌이 올 것이다. 


소스 코드는 다음과 같다.

  glEnable(GL_BLEND);  // 블렌딩활성화

  glBlendFunc(GL_SRC_ALPHA,GL_ONE); 

 

  glBegin(GL_TRIANGLES);  // Drawing Using Triangles

  glColor4f(1.0f, 1.0f, 1.0f, 0.33333f);

  glVertex4f( 0.0f, 1.2f, 0.0f, 1.0f); 

  glVertex4f(-1.2f,-1.0f, 0.0f, 1.0f); 

  glVertex4f( 1.2f,-1.0f, 0.0f, 1.0f); 

  glEnd(); 

 

  glBegin(GL_QUADS);  // Draw A Quad

  glColor4f(1.0f, 1.0f, 1.0f, 0.33333f);

  glVertex4f(-1.0f, 1.0f, 0.0f, 1.0f); 

  glVertex4f( 1.0f, 1.0f, 0.0f, 1.0f); 

  glVertex4f( 1.0f,-1.0f, 0.0f, 1.0f); 

  glVertex4f(-1.0f,-1.0f, 0.0f, 1.0f); 

  glEnd(); 


  glColor4f(1.0f, 1.0f, 1.0f, 0.33333f);

  glutSolidSphere(1.0f, 36, 36);

posted by 대갈장군