블로그 이미지
대갈장군

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

2016. 2. 8. 23:04 프로그래밍/C++

출처: http://duartes.org/gustavo/blog/post/getting-physical-with-memory/

저자: Gustavo Duarte


이 글도 상당히 도움이 되는 글인듯 하여 번역하여 옮겨둔다.


원래 복잡한 시스템을 이해하려면 가장 기초가 되는 원초적인 것을 들여다 볼 필요가 있다. 그런 점에서 우리는 프로세서와 버스 간의 인터페이스에 대해서 알아보자... 저자는 EE 전공자가 아니므로 EE 친구들이 걱정하는 그런 부분은 가뿐히 무시하겠다고... CS! CS! /yay


Physical Memory Access

위의 그림은 Core 2 디자인이다. 코어 2의 경우 775개의 핀을 가지고 있다. 그중 절반은 파워만 제공하고 데이터를 운송하지 않는다. 그럼 대략 387.5 개 인가? 작동하는 핀들을 그룹으로 묶어주면 사실 코어2의 디자인은 상당히 단순하다. 위의 그림을 보면 중요한 핀들을 보여주고 있는데 Address pin, Data pin, Request pin 들이다. 이 동작들은 FSB (Front Side Bus)라 불리는 곳에서 이른바 Transaction이라는 이름으로 시행되는데 FSB Transaction은 5개의 페이즈를 가지고 있단다. Arbitration, Request, Snoop, Response, Data 요렇게 다섯가지. Agent라 함은 프로세서에 Northbridge까지 포함한다. 한국말로 요원들이라고 해야 겠군... 이 요원들이 다섯가지 페이즈를 호출하여 사용하는데 각각 다른 형태의 임무를 수행한다는 군...


우리가 관심있는 분야는 오직 Request Phase다. Request Agent (주로 한개의 프로세서) 에 의해 2개의 packet이 출력으로 나오는 페이즈가 바로 Request Phase. 

FSB Request Phase, Packet A

위 그림처럼 35-3 (33 비트)의 길이를 가지는 필드를 먼저 보게 되는데 이것은 물리 주소를 알려주는 필드이다. 그리고 바로 뒤에 REQ 핀이 오는데 이 녀석의 각 비트 값에 따라 어떠한 형태의 접근 및 처리를 원하는지 알려준다. 이 첫번째 패킷이 나간 후에 바로 두 번째 패킷이 나가는데 아래 그림과 같다.

FSB Request Phase, Packet B

여기서 재미 있는 부분은 바로 Attribute Signals 필드인데 (31:24) 여기를 보면 메모리 캐싱 동작의 5가지 종류에 대해서 정의하고 있다. FSB에 이 정보를 제공함으로써 Request Agent는 다른 프로세서들에게 이 Transaction이 그들의 캐시에 영향을 주는지 알려주며 메모리 컨트롤러인 Northbridge에게 어떻게 동작해야 하는지도 알려준다. 프로세서는 커널에 의해 작성되는 page table을 참조하여 주어진 메모리 영역의 타입을 결정한다.


일반적으로 커널은 모든 RAM 영역을 write-back으로 간주하는데 그것이 최상의 퍼포먼스를 내기 때문이다. Write-back 모드에서는 메모리 액세스의 단위가 cache line 이 되고 이것은 64 바이트이다 (코어2에서). 만약 하나의 프로그램에서 1 바이트를 메모리에서 읽으면 프로세서는 그 1바이트를 담고 있는 전체 cache line 을 L2 와 L1 캐시에 로드한다. 또한 프로그램이 메모리에 쓰는 경우, 프로세서는 캐시에 있는 라인만 수정하고 main memory는 업데이트 하지 않는다. 나중에, 처리해야 하는 시점이 오면 그제서야 cache line 전체를 한방에 메인 메모리로 써버린다. 고로 대부분의 request는 Length field가 11 (64 bytes) 값을 가지고있다. 다음 그림을 보면 캐시에 없는 데이터를 어떻게 읽어들이는지 보여준다.

Memory Read Sequence Diagram

위 그림을 보아하니 대충 이해가 간다. 프로그램이 특정 주소를 달라고 요청하게 되면 우선 L1 캐시에 있는지 확인해보고 없으면 L2로 가고 거기도 없으면 FSB로 요청해 그 주소로 직접적인 액세싱을 하게 된다는 거. 길이는 64바이트로 고정되어 있군. 


추가적으로 인텔 컴퓨터의 일부 메모리 영역은 장치로 연결된 경우가 있다. 즉, 메모리 영역이 실질적인 RAM이 아니라 하드 드라이브의 일부이거나 네트워크 상의 공간일 수 있다는 것인데 이런경우에는 커널이 이런 주소 영역을 uncacheable 로 도장을 쾅 찍어둔다. 당연히 이런 주소 공간을 캐시에 담아둔다는 것은 말이 안된다. 이런 경우에는 길이가 64바이트가 아닐수 있으므로 Length field가 64 바이트가 아닐수 있다는 점..


글의 결론은

1. 퍼포먼스가 중요한 프로그램의 경우에는 같은 캐시 라인 안에 필요한 데이터를 팩킹해서 처리하도록 노력하는 것이 성능향상에 도움이 된다. 캐시에서 바로 가져오는 경우 성능의 향상이 어마어마 하다.

2. 하나의 cache line 안에 포함되는 메모리 엑세싱Atomic 이 보장된다. 원소성이라고 하는데 이것이 좋은 이유는 중간에 다른 스레드가 개입하여 중단하는 일이 없기 때문이다. 즉, 멀티 스레드에도 안전성을 제공한다는 거.

3. FSB는 모든 에이전트에 의해 공유된다. 모든 에이전트는 모든 transaction을 듣고 있어야 하는데 이런 것이 FSB에 교통혼잡을 유발하고 성능을 떨어뜨리는 이유가 되었다. 바로 Core i7에서는 이러한 문제를 해결하기 위해 프로세서가 바로 메모리로 접근할수 있도록 변경했다. 그것이 성능 향상에 큰 도움.






posted by 대갈장군
2016. 2. 6. 05:55 프로그래밍/C++
출처: http://duartes.org/gustavo/blog/post/anatomy-of-a-program-in-memory/

저자: Gustavo Duarte


요즘 메모리 관련 문제를 다루다 보니 점점 더 힙 속으로 빠져들고 있던 차에 좋은 글을 발견하고 내 개인 소장용으로 옮겨 볼까 한다.


일반적으로 멀티 테스킹 OS에서 각각의 프로세스는 그들 자신만의 메모리 샌드 박스에서 실행된다. 즉, 각각의 프로세스가 개별적인 주소 공간을 소유한다는 의미... 이 샌드 박스는 일반적으로 32비트 환경일 경우 Virtual Address Space라 부르고 약 4GB의 메모리 공간이 주어진다. (32비트니까~)


이 가상 메모리 주소 공간은 실질적인 (물질적인) 메모리와 이른바 Page Table이라는 놈을 통해 맵핑 되는데, 뭐, 쉽게 말해서 가상 주소는 말그대로 가상으로 주어진 공간이고 실제 메모리가 할당되려면 맵핑이 필요한데 이 주소록을 가지고 있는 녀석이 Page Table이라는 거지. 재미 있는 점은, 컴퓨터에 돌아가는 모든 프로세스는 이 룰에 따라야 한다는 말이다. 심지어 커널 자신 조차도... 쉽게 생각하면 커널은 예외일 것 같지만 그렇지 않다는 점을 글쓴이는 말하고 싶었던가 보다. 아무튼, 그래서 이 가상 주소 공간은 반드시 커널을 위해 일부를 쓴다는 점이다. (아래 그림처럼)

Kernel/User Memory Split

리눅스는 기본적으로 1GB만 커널이 사용하도록 기본 셋팅이 되지만 윈도우는 2GB나 커널을 위해 할당한다. 즉, 프로세스 자신은 남은 2GB만 사용가능하다는 거네... 이런 위도우 같은 경우를 봤나... 물론 윈도우도 3GB까지 확장하는 옵션이 있단다... 휴..


뭐 이쯤에서 눈치 챘겠지만 각 프로세스가 4GB씩이나 할당하면 나의 구린 컴퓨터는 고작 16GB 램인데 프로세스 4개 돌리면 램 바닥찍고 끝나는가 라는 의문이 스멀스멀 든다면 4 곱하기 4는 16이므로 정답..


물론, 각각의 프로세스가 4GB를 무조건 쓴다는 건 아니라는 점... 시작은 미미하게 해서 끝은 창대해 질수 있겠지만 일반적으로 시작할때는 최소 필요 메모리만 할당해서 최대 4GB까지 쓸수 있다는 점을 이론적으로 보여주는 것일 뿐, 실제로 프로세스가 무조건 4GB 할당하고 출발하는 건 아니라는 점.


다만, 프로그래머가 알아야 할 것은 실수로 커널 메모리 영역을 침범하면 운영체제가 "네 이놈! 여기가 어디라고 감히 들이대느냐!!" 하면서 프로그램을 종료시켜 버린다는 거... 또 한가지 중요한 점은, 커널 코드와 데이터는 항상 어디서나 접근 가능하고 인터럽트를 핸들링 하며 시스템 콜을 받아들일 준비가 되어 있는 반면, 사용자 주소 영역은 프로세스가 변경되면 그에 따라 변화한다는 점... (아래 그림)

Process Switch Effects on Virtual Memory

위 그림에서 사용자 주소 영역에 하늘색 표신 된 부분은 메모리가 할당되어 사용되어진 영역이고 흰색은 할당되지 않은 영역이다. 재미 있는 건, 저자가 Firefox는 메모리를 무지하게 잡아 먹는다면서 저렇게 Firefox의 메모리 할당을 크게 그렸다는 점... 크롬이 더 많이 먹는거 아닌가 몰라...

Flexible Process Address Space Layout In Linux

위 그림을 보면 대략적인 프로세스의 주소공간을 (4GB) 볼 수 있다. 일반적으로 모든게 제대로 잘 작동했을 경우 대부분의 프로그램은 위의 그림과 같은 구조의 메모리 공간을 가지고 시작하게 된다. 사실 이점에 대해서 저자는 보안 취약점이 될 수 있다고 지적하고 있다. 해커가 이런 메모리 구조를 미리 파악하고 대충 찍어서 맞추는 경우 보안상의 헛점이 드러날 수 있다는 점. 그래서 요즘은 메모리를 랜덤하게 유행한단다... 이 글이 써진 시점이 무려 2009년이니... 허허


위 그림에서 사용자 공간의 최상층에 위치하고 있는 것이 바로 Stack인데 이 녀석은 지역 변수를 저장하고 함수의 인자들을 저장하는 용도로 주로 사용된다. 함수를 호출하게 되면 스택영역에 Stack Frame이라는 놈을 넣게 되는데 이 함수가 리턴 될 때 이 Stack Frame도 파괴된다. 스택 자체가 LIFO 방식으로 단순하게 동작하므로 pushing과 popping이 매우 빠르고 효율적으로 수행 가능하다. 그리고 스택 영역을 자주 그리고 많이 사용하면 결국 CPU 캐시에 스택 메모리가 활성화 되어 있게 되므로 스피드를 더더욱 업업업~ 각각의 스레드는 자신만의 스택을 가진다.


만약 사용자가 스택 영역에 할당된 현재의 영역보다 더 큰 데이터를 집어 넣게 되면? 이렇게 되면 (리눅스의 경우) expand_stack()이라는 함수 (결국, acct_stack_growth() 함수 호출하는 녀석)를 호출해서 스택 영역의 확장이 가능한가 체크한다. 만약 스택 영억이 기본 최대 사이즈 (8MB)보다 작다면 스택 영역이 확장된다. 근데, 만약 이 8MB를 다 써버리면? 그때 바로 터지는 것이 Stack Overflow이고 프로그램은 Segmentation Fault를 받으면서 장렬히 전사한다. 또 한가지 흥미로운 점은 스택 영역은 한번 확장되면 스택이 줄어든다고 해서 확장된 영역을 줄이진 않는다. 이것이 마치 미국의 연방 정부의  예산과 같다고... 늘리기만 하고 줄이진 않는다는 이야기.. ㅋㅋ


자, 그 다음은 스택 아래에 위치한 Memory Mapping Segment라는 놈인데 이 놈은 파일의 내용을 직접적으로 메모리로 맵핑해준다. 이런 일을 해주는 대표적인 함수로는 mmap(), CreateFilemapping(), MapViewOfFile() 같은 함수들이다. 이러한 Memory Mapping은 고성능 파일 입출력이나 Dynamic library들을 로딩하기 위해서 사용되어 진다. 그리고 이른바 Anonymous Memory Mapping도 가능한데 이건 임의 메모리 맵핑이라 해야 하겠네.. 파일이나 라이브러리 같은걸 맵핑 하는게 아니라 malloc() 같은 함수가 제법 큰 메모리 공간을 할당하려고 할때 종종 이 구역의 메모리를 임의 메모리 맵핑이라는 형태로 사용하도록 해준다는 군. 메모리를 좀더 효율적이고 많이 사용하기 위해 그런듯...


그다음은 바로 힙! 엉덩이 아니지요.. Heap! 사실 프로그램 돌리다 보면 터지는 메모리 에러의 90% 이상이 힙에서 터진다고 봐도 과언이 아니다. 이 힙 영역은 프로그램이 런타임 (실행중) 중에 메모리를 임의의 크기로 할당할때 사용되는 영역이다. 이러한 동적 메모리 할당 때문에 힙 메모리 사용은 커널과 언어의 런타임이 서로 협상해서 만들어 진다. 대표적인 힙 할당 함수로 C에서는 malloc이 있고 C#이나 C++의 경우에는 new가 있다. 


만약 메모리 공간이 여유가 있다면 언어의 런타임에서 알아서 처리하고 끝낸다. 만약 현재의 할당된 힙으로 공간이 부족하다면 시스템 콜 (brk())을 통해 확장되는데 이것이 바로 커널의 개입이다. 사실 힙의 관리는 정말 복잡한데 머리 좋고 똑똑한 사람들이 최선을 다해 알아서 잘 관리하도록 만들어 놨다고... 뭐, 아무튼...


그리고 마지막으로 제일 아래 영역이 보이는데 BSS, data 그리고 program text다. BSS와 data store의 경우 static global variable들을 저장하는 용도로 사용된다. BSS는 할당되지 않은 (uninitialized) static variable들을 저장하고 data segment의 경우에는 initialized된 static variable들을 저장한다. 


뭐 몇가지 설명이 더 있기는 한데 그렇게 필요해 보이진 않아서 스킵~ 




posted by 대갈장군
2014. 8. 28. 04:47 프로그래밍/C++

출처: http://ikpil.com/540


내가 C++에 조예가 깊어서 글을 남기는 것이 아니라, Effecitve C++ 을 공부하는 사람들이 이 글을 보고, 도움이 되었으면 하는 생각과, 혹시 내가 틀린것이 있다면 지적해 주시지 않을까 란 생각으로 글을 올리는것임을 미리 밝힙니다.  - 최익필


typename .. 뭐 이렇게만 보면 이런게 있었나 싶다. 하지만 템플릿에서는 흔하게 보는 키워드인데, 이 키워드에 대해서 제대로 파악하자고 하는것 같다.

처음부터 진행하자면 typename 과 class 는 똑같은 의미인데, 이렇게 말만 하면 혼동의 요지가 있으니, 코드도 포함해 주는 센스를 발휘해 본다.

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
// ikpil.com or ikpil.tistory.com
 
#include <iostream>
//template <typename _T>  // class _T 와 같은 의미
template <class _T>
class babo
{
typedef _T value_type;
public:
    void Draw( void )
    {
        std::cout << sizeof(value_type) << "의 용량을 가진 클래스" <<std::endl;
    }
private:
    _T *p;
};
 
int main( void )
{
    // int 타입을 가진 babo 클래스의 객체 Test 선언
    babo<int> Test;
     
    // 뭐~ 출력
    Test.Draw();
 
    return 0;
}


위의 주석을 다 읽어 보면 무슨 말인지 알듯 싶다. 하지만 스콧마이어어가 쓰고, 곽용재씨가 번역한 이번 항목에선 typename 은 또 다른 의미를 지니고 있다고 알려 주려고 하는듯 하다.

그렇다 typename 키워드는 .. ... 은 타입이다! 를 알려 주는 용도로 사용 한다. 요렇게만 들으면 또 .. 햇갈린다. 더 이야기 하자면 typename 키워는 템플릿 선언 내부에서만 사용 할수 있다는 것을 기억해야 한다. 아래 소스코드를 첨부 하겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// ikpil.com or ikpil.tistory.com
int main( void )
{
    // int 타입을 가진 babo 클래스의 객체 Test 선언
    babo<int> Test;
     
    typename int a; // 컴파일 에러를 볼수 있을 것이다.
 
    // 뭐~ 출력
    Test.Draw();
 
    return 0;
}


그렇다면 이것은 템플릿 내부에서 써보자면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ikpil.com or ikpil.tistory.com
 
#include <iostream>
template <typename _T>    // class _T 와 같은 의미
class babo
{
typedef _T value_type;
public:
    void Draw( void )
    {
        std::cout << sizeof(value_type) << "의 용량을 가진 클래스" <<std::endl;
    }
private:
    _T *p;
public:
    typename int hehehe;    // 이렇게 쓸수 있다.
};


이렇게 쓸수 있다는 것인데, 이것만 본다면, 
1. int 는 타입이다
2. 그 타임으로 a 객체 생성!

.. 이것을 보고 비웃지 않는다면, 당신이야 말로 성인(成人)이다. 왜냐하면 int 는 누가봐도 타입인데 굳이 typename 을 피곤하게 붙인다는 것에 대해서 말이다.

그렇다면 다음 코드는 어떻게 이해 할 것인가.

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
// ikpil.com or ikpil.tistory.com
#include <iostream>
#include <vector>
 
template <typename _T>    // class _T 와 같은 의미
class babo
{
public:
    void Draw( void )
    {
        std::cout << sizeof(p) << "의 용량을 가진 클래스" <<std::endl;
    }
private:
    _T *p;
public:
    _T::iterator muhaha; // 이것 때문에 컴파일이 되지 않는다.
};
 
int main( void )
{
    // int 타입을 가진 babo 클래스의 객체 Test 선언
    babo<std::vector<int> > Test;
     
    // 뭐~ 출력
    Test.Draw();   
 
    return 0;
}


.. 물론 나는 _T의 타입인 iterator 란것을 분명이 알고 있다. 하지만 컴파일러는 다르다. 어떤 초특급 울트라 천재가 컴파일러를 만든다 하더라도, .. 저것이 "타입" 인것을 .. 알려면 몇달 몇일 간 별 수를 다 생각해야 할것이다. 

_T::iterator muhaha; 이것을
typename _T::iterator muhaha; 라고 바꾸면 정상적으로 컴파일이 되는 것을 볼수 있을것이다. 그렇다! typename은 템플릿 내부에서 사용하면서, 컴파일러에게 템플릿 내부에서 "이건 타입이야" 라고 알릴 필요가 있을 때 써준다.


이쯤에서 용어 설명을 하는게 좋을듯 싶은데,
_T 처럼 타입을 인자로 받아 그것을 타입으로 쓰기 때문에 _T를 의존 이름(dependent name) 이라 외국에서 불린다.요~런 사항에서 의존 이름(dependent name)속에 또 다시 다른 이름이 정의된 경우 그녀석은 중첩 의존 타입 이름(nested dependent type name .. 뭐 외국 따라가야지, .. 태생이 외국인인데..)이라고 불리는데,

바로 템플릿 내부에서 중첩 의존 타입 이름(nested dependent type name)이 있을 경우, 그녀석도 타입인것을 인지하지 못하는 사태가 발생 할 수 있다.(발생 할수 있다는 이유는 MSVC2005에선 발생하지만, 다른 컴파일러에선 발생 안할수도 있다는 뜻이다. 뭐.. 대부분은 발생한다.)

여담으로 int 같은 것들은 비의존 이름(non-dependent name)이라고 불린다. .. 뭐 그런게 있다~ 라고 알아두면 좋을듯 싶고.. 여기서 한가지 짚고 넘어가야 한다. 이런 typename 을 쓸때, template 내부여도 못쓰는 경우가 두가지 있다는 것이다!

이 경우는 중첩 의존 타입 이름(nested dependent type name)이 기본 클래스의 리스트에 있거나, 멤버 초기화 리스트 내의 기본 클래스 식별자로 있을경우 typename을 붙여 주면 안된다는 것이다!(사실 이 구역은 컴파일러가 알아서 이녀석은 타입이다! 라고 알수 있는 곳이기도 하다.) 이렇게만 말하면 다시 .. 난해 할 수 있으니 코드를 달아 주는 센스!

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// ikpil.com or ikpil.tistory.com
// typename 을 예외적으로 사용 할수 없는
// 구역을 나타내기 위해서 이렇게 코드를 만든다.
 
#include <iostream>
#include <vector>
 
template <typename _T>
class NON_TYPENAME
{
public:
    // 뭐 클래스 네임만 신경쓰고 나머진
    // 무시해도 된다.
    NON_TYPENAME() { }
    // 소멸자
    ~NON_TYPENAME() { }
private:
    _T m_p;
};
 
 
template <typename _T>    // class _T 와 같은 의미
class babo
    : public NON_TYPENAME<_T> // <-- 여기가 기본 클래스의 리스트
{
public:
    babo()
        : NON_TYPENAME<_T>() // <-- 여기가 멤버 초기화 리스트
    {
 
    }
    void Draw( void )
    {
        std::cout << sizeof(p) << "의 용량을 가진 클래스" <<std::endl;
    }
private:
    _T *p;
public:
    typename _T::iterator muhaha;   // <-- 여긴 사용 할수 있다~
};
 
int main( void )
{
    // int 타입을 가진 babo 클래스의 객체 Test 선언
    babo<std::vector<int> > Test;
     
    // 뭐~ 출력
    Test.Draw();   
 
    return 0;
}


자.. 이제 이해가 되었는가? 여기에 코딩 스타일이 좀더 편해질만한 이야기를 하나 더 들려 준다.
바로 typename _T::iterator muhaha; 를 쓸때 한번이나 두번 쯤이야 써주겠지만 3 이상 넘어가면 .. 슬슬 피곤해 진다. 이럴 때는 typedef typename _T::iterator iterator; 라고 쓰고 iterator muhaha; 라고 쓰면. 뭐 쉽지 않니한가? ㅋㅋ


여담으로 몇가지 더 이야기를 한다.
이 이야기는 곽용재씨가 들려주는 이야기로 스콧 마이어스와 허브서터, 안드레 알렉산드레스쿠는 사용자가 정의한 타입에 대해서는 class 를 쓰고 기본 제공 타입에 대해서는 typename 을 쓴다. 그리고 비야네 스트롭스트룹은 class 를 주로 쓰는 편이라고 한다;

이렇게 굳이 꼭 정해 둘 필요가 있을까? 하는 사람도 있지만, 코드가 몇만줄 넘어가면, 조금이라도 코드에 정보 아닌 정보를 남겨두는 편이, 디버깅이나 유지보수 등에 도움이 될때가 많다. 이런 습관도 있으니 배워두자는 취지에서 곽용재씨가 남겨 주셧던것 같다.


이것만은 잊지 말자!
1. 템플릿 매개변수를 선언할 때, class 나 typename 이나 똑같다.
2. 중첩 의존 타입 이름(nested dependent type name)을 식별하는 용도는 typename 키워드이다.
3. typename 은 템플릿 내부에서만 사용 되고, 초기화리스트 및 기본 클래스 리스트에서는 못쓴다!


관련링크
http://www.ibm.com ... typename keword <-- ibm의 typename 설명서 아, 영문이다 제길
http://pages.cs.wisc.edu/~driscoll/typename.html <-- 여기도 외국 . typename 설명서 .. 영문이다.
http://keiphoto.egloos.com/4182181 <-- 짤막한 .. 영문으로.
http://kukuta.tistory.com/12 <-- 오오! 괜찮은 설명~ 아 여긴 한국
http://codefactory.zc.bz/50 <-- 댓글이 설명이 좋았음
http://rein.upnl.org/wordpress/archives/494 <-- typename 관련 글은 아니지만, 템플린 관련해서 좋은 글
http://www.filewiki.net/tc/34 <-- typename 을 어떤 식으로 사용하는지 소스로 확인 할수 있음
http://dblab.co.kr/119 <-- 역시 마찬가지
http://kldp.org/node/62341 <-- typename 문법 없이 사용할 경우, 질문 내용
http://kldp.org/node/39641 <-- 역시 질답 내요이 좋음

posted by 대갈장군
2012. 12. 18. 03:51 프로그래밍/C++


int _tmain(int argc, _TCHAR* argv[])

{

       TCHAR * str = NULL;

       __try

       {

              str = (TCHAR *)malloc(20);

       }

       __finally

       {

              if(str != NULL)           

              {

                     free(str);

                     str = NULL;

              }

       }

       return 0;

}


예외 처리는 안전한 소프트 웨어를 만들기 위한 기본인데 몇가지 주의해야 한다.


우선 __try 내부로 들어온 다음 __finally 를 무조건 통과해서 나가는 것이 기본적인데, 주의해야할 점은 어떤 알 수 없는 오류로 인해 __try 내부의 메모리 할당이나 리소스 할당이 실패하는 경우, __finally 내부에서 무조건 메모리나 리소스를 해제 하려고 하면 '선언'은 되었지만 '초기화'가 되지 않은 포인터의 경우에는 "Run-Time Check Failure #3 - The variable 'str' is being used without being initialized." 라는 에러가 발생한다.


초기화 되지 않았으므로 알 수 없는 값을 가지고 있어서 그렇다. 이런 문제를 막으려면 애당초 포인터를 선언 할 때, NULL 값을 넣어주면 __finally 내부에서 if 로 NULL인지 아닌지를 체크하는 문법이 문제가 없이 잘 동작한다.


예외 처리를 잘 해야 하는 또 다른 이유는 하드웨어나 예상치 못한 문제로 프로그램이 이상 작동을 할 때 프로그래머의 잘못이 아님을 보여줄 수 있는 최후의 방패이기 때문이기도 하다.





posted by 대갈장군
2012. 12. 15. 04:59 프로그래밍/C++

http://msdn.microsoft.com/en-us/library/dd293575(v=vs.100).aspx


오, 생각보다 간단하게 어플리케이션 재배포 패키지를 만드는 방법이 있넹? 뭐 아주 완벽하지 않지만 간단하고 빠르게 설치파일을 생성해 낸다는 점에서 꽤 괜찮은듯.  


우선, 내가 만든 프로그램 이름이 MyMFCApplication.exe라고 치자. 그러면 우선적으로 만들어야 할 파일이 setup.bat 파일이다. 이 파일에 다음과 같은 문장을 때려 넣자.

@echo off
vcredist_x86.exe
mkdir "C:\Program Files\MyMFCApplication"
copy MyMFCApplication.exe "C:\Program Files\MyMFCApplication"


뭐 간단히 예기하자면 도스 모드에서 실행될 명령어를 주욱 적어 놓은 것인데, 우선 vcredist_x86.exe파일을 실행하라는 말이고 이어서 "C:\Program Files\MyMFCApplication" 폴더를 생성하라는 말이고 다음으로는 내가 만든 어플리케이션을 복사해서 거기다 집어 넣으라는 말이다.


이게 왜 필요한지는 조금 있다 알게된다.


일단 cmd 윈도우를 열고 iexpress.exe를 치고 엔터! 그러면 아래와 같은 녀석이 나타난다.



Create new Self Extraction Directive File을 선택한 후 Next!


그 다음에는 Extract files and run an installation command 선택 후 Next!


그 다음에는 내 프로그램의 이름을 묻는데 내가 원하는 이름을 적어 놓고 Next!


그리고 Confirmation prompt 페이지에서는 No Prompt를 선택하고 Next!


다음으로 License agreement인데 Do not display a license를 선택 하고 Next!


이제 Packaged files 페이지에 도착했는데 여기서 이제 내가 만든 프로그램과, vcredist_x86.exe 파일과 아까 만들어 둔 setup.bat 파일을 넣어준다. 참, vcredist_x86.exe 파일은 Program Files 폴더 아래에 \Microsoft SDKs\Windows\v7.0A\Bootstrapper\Packages\vcredist_x86  에 가면 찾을 수 있다.



 

 위 그림에 보면 내가 만든 프로그램은 빠져 있는데 넣는걸 잊어 먹지 말자.. ㅋㅋ


그리고 다음을 눌러보면 Install Program to Launch 페이지에 도착하는데 거기서 Install Program 텍스트 박스에다가 다음과 같이 넣어준다.


cmd.exe /c "setup.bat"





뭐, 저 명령은 cmd (도스 윈도우) 띄워서 setup.bat 파일 실행하라는 말이다. 


그 다음으로 Show Window 페이지에서는 Default 선택 후 Next!


Finished message 페이지에서는 No message 선택 후 Next!


그 다음인 Package Name and Option Pages에서는 내가 원하는 Setup 파일 이름을 적되, 아래 옵션에서 Store files using Long File name inside Package 옵션을 반드시 켜고 둘째로 파일 이름의 끝에 Setup을 꼭 넣어줘라. (예를 들자면 MyMFCApplicationSetup.exe)





이제 거의 끝이 보인다... 그리고 Configure restart 페이지에서는 No restart 선택 후 Next! (설치후 재시작 할꺼냐는 말)


Save Self Extraction Directive 페이지에서는 Save Self Extraction Directive (SED) file 선택 후 Next!


그리고 최종적으로 Create package 페이지에서 Next 클릭!


이제 내가 iexpress.exe를 실행했던 폴더를 가보면 뭔가 이상한 파일이 하나 만들어져 있을 것이다. 





두둥. 이것이 인스톨 파일이다! 짜짠...


이걸 이제 내가 설치하고자 하는 컴퓨터에 복사해서 더블 클릭해보면 우선 vcredist_x86.exe가 실행되면서 해당 컴퓨터에 Visual C++ redistribution 패키지 (DLL파일들)를 자동 설치하게 된다.


그 다음에는 프로그램 폴더에 자동으로 폴더를 생성해서 내가 만든 어플리케이션을 거기다 복사해 준다. 필요한 파일들이 더 있다면 복사 명령을 더 setup.bat에 더 써넣고 패키지에 필요 파일들을 더 넣어주면 될것 이다.


왜 이런 방법이 있는지 몰랐지? ㅋㅋ 역시 MSDN은 길을 잃고 헤멜 필요가 있는 미로다. 




posted by 대갈장군
2012. 12. 15. 04:17 프로그래밍/C++

http://msdn.microsoft.com/en-us/library/8kche8ah(v=vs.100).aspx


이글은 Visual Studio 2010 C++을 겨냥한 것임을 미리 밝혀둔다.


알다시피 내 컴퓨터에서 작성한 프로그램이 다른 컴퓨터에서 잘 작동할 것이라는 핑크빛 꿈을 꾸는 경우가 많은데 현실은 정반대다. 그렇다면 내가 내 컴퓨터에서 작성한 코드가 다른 사람 컴퓨터에서 잘 돌아가게 하려면 무슨짓을 해야 할까? 정답은 DLL을 프로그램이랑 같이 포장해서 보내줘야 한다는 것인데, 어떤 DLL이 필요한지는 어떻게 알까? 


그중에 한 방법이 Dependency Walker를 이용해 내가 만들 어플리케이션의 필요 DLL들을 검색해보는 것이다.


사진을 첨부하려 했는데 첨부가 안되네~ 아무튼, Dependency Walker를 사용하면 해당 프로그램에 필요한 DLL 목록이 주루루룩 뜬다. 뭐 경고 중에는 가볍게 무시해도 괜찮은 것들도 많지만 Delay-load Dependency나 Explicit (Dynamic) Dependency가 아닌데 못찾겠다고 나오는 DLL들은 뭔가 문제가 있을 확률이 높으니 눈여겨 봐야 할 필요가 있다.

 

본인이 작성한 프로그램이라면 어떤 LIB 파일이 프로그램에 링크 되었는지 알고 있으므로 운영제체나 .NET 프레임워크에서 공통적으로 지원하는 공유 DLL을 제외하고 어떤 기타 DLL이 반드시 필요한지도 일반적으로는 알고 있어야 정상이다.

 

다음 표를 보면 어떤 DLL이 어떤 경우에 필요한지에 대해서 간단하게 설명하고 있다.

 

Visual C++ Library

Description

Applies to

atl100.dll

Active Template Library (ATL).

Applications that use ATL.

msvcr100.dll

C Runtime Library (CRT) for native code.

Applications that use the

 C Run-Time Libraries.

msvcp100.dll

Standard C++ Library for native code.

Applications that use the 

Standard C++ Library.

mfc100.dll

Microsoft Foundation Classes (MFC) Library.

Applications that use

 the MFC Library.

mfc100u.dll

MFC Library with Unicode support.

Applications that use the

 MFC Library and require 

Unicode support.

mfcmifc80.dll

MFC Managed Interfaces Library.

Applications that use the 

MFC Library with

 Windows Forms Controls.

mfcm100.dll

MFC Managed Library.

Applications that use the

 MFC Library with

 Windows Forms Controls.

mfcm100u.dll

MFC Managed Library with Unicode support.

Applications that use the

 MFC Library with 

Windows Forms Controls and

 require Unicode support.

 

그런데 마지막 문단에 보면 이런 말이 있다. 일반적으로 시스템이 제공하는 DLL들 예를 들자면 Kernel32.dll, User32.dll, Ole32.dll 같은 놈들은 원래 재배포가 필요가 없다. 왜냐면 윈도우즈를 쓰는 모든 컴퓨터에는 기본적으로 저놈들이 있게 마련이기 때문이다. 하.지.만! 문제는 윈도우가 여러 버젼이 있다는 점이다. 윈도우 7과 윈도우 7 서비스팩 1과는 많은 차이가 있다. 고로 이런 경우 같은 운영체제이다고 하더라도 프로그램이 안돌아 갈수 있다는 점! 고로, 반드시 업데이트를 통해서 업그레이드 하거나 아니면 마이크로소프트에서 제공하는 각종 리디스트리뷰션 패키지 (재배포 설치 파일들)를 통해서 필요한 부분을 설치해야 한다!

 

저거 생각보다 중요한 점이라는 점!










posted by 대갈장군
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 대갈장군
2009. 7. 28. 02:48 프로그래밍/C++
나는 늘 같은 경험을 한다. 모르는 어떤 것을 알기위해 이것 저것 읽어 보다보면 끊어진 이해의 연결 고리들이 어느순간 한번에 이어지면서 한순간에 전체적인 흐름을 파악한다.

그 흐름이 파악되면 세부적인 내용은 너무나도 쉽게 쉽게 이해가 된다. 아무리 어려운 문법이라도 '음, 이건 분명 이렇게 될 수 밖에 없지...'라는 마치 알고 있었다는 듯한 느낌마저 받는다.

STL도 처음 나에게는 '이거 뭐야?' 라는 놈이었지만 지금은 '음, 이놈 복잡한 놈...' 이라는 정도의 느낌만 줄뿐 아예 모르는 놈은 아니다.

STL을 알기위해서는 템플릿도 알아야 하고 포인터에 대한 이해 및 함수 포인터 그리고 배열 및 객체 지향에 대한 이해도 필요하다.

하지만 이것은 실제적인 사용을 위해 필요한 것이고 이보다 앞서 필요한 '이해'를 위해서는 아무것도 필요없다. 무엇인가 알려고 할때는 항상 전체적인 흐름을 파악하고 왜 이것을 알아야 하는가에 대해서 먼저 답을 해야 한다.

그것을 알지 못하고 무작정 세부적인 문법만 파고들면 금새 길을 잃어버린 아이처럼 이곳 저곳을 방황하다가 결국 지쳐 울고 만다.

그렇다면 왜 사람들이 STL, STL하는지 알아보자. 과연 무엇이 그리도 좋다는 말인가?

STL은 한마디로 라이브러리다. 라이브러리... 도서관... 즉, 필요한 모든 정보가 있는 곳이다. 사용자는 이 STL을 이용해 수많은 똑똑한 개발자들이 만들어 놓은 아주 편리한 함수들을 손쉽게 꺼내어 사용할 수 있다.

프로그램을 작성하기 위해 필요한 두가지 필수 요소는 바로 알고리즘자료구조이다. 프로그램은 항상 저장된 데이터를 불러와 어딘가에 저장해 놓고 (자료구조) 그 값들을 필요에 따라 정렬, 삭제하여 (알고리즘) 원하는 것을 하는 것이다.

바로 이 두 가지 필수 요소인 자료구조와 알고리즘을 모조리 묶어서 정리한 라이브러리가 바로 STL이다. 

하지만 여기서 그치지 않는다. STL은 이름에서 처럼 Standard (일반화) 특성을 가지고 있다. 즉, 어떠한 자료구조이던 간에 상관없이 동일한 형태의 알고리즘을 제공한다는 점이다. 

이로써 사용자가 각 자료구조에 대한 각각의 알고리즘 (정렬, 삭제, 추가, 대입등) 을 일일히 정의해 줄 필요가 없어진 것이다.

이 얼마나 고마운 일인가...

게다가 STL은 이름에 있듯이 Template를 이용해 사용자가 원하는 임의의 형태로 객체를 생성해 낸다. 즉, int를 원하면 int, float을 원하면 float, 클래스를 원하면 클래스... 말 그대로 지 맘대로 객체를 생성할 수 있다.

고마워서 눈물이 날 지경이다... 상상을 해보라. 프로그래머가 자신이 사용해야 할 자료구조 (스택, 리스트, 큐) 같은 것을 프로그램 짤때마다 만들어야 한다면? 게다가 알고리즘까지 다 만들어야 한다면??

이런 복잡한 일을 다 해주는 고마운 친구가 STL인것이다.

그럼 아마도 이런 생각이 들것이다. 그럼 왜 진작부터 이거 사용안하냐고... 

좋은 질문이다... STL은 이해하기는 쉽지만 사용하기 위해서는 C++과 C에 대한 전반적인 이해가 필수이며 몇몇 단점들이 존재하기 때문이다.

우선 실행파일이 거대해 진다. 원래 STL은 속도만을 중요시 하기때문에 컴파일 타임에 링크하는 방법을 쓰지 않는다. 고로 정적 링크를 항상 하게 되므로 생성하는 STL의 객체 수와 사용하는 STL 함수만큼 정적 링크의 갯수가 늘어간다.

하지만 크기의 이점을 잃어버리는 대신 '편리함'을 얻었으니 불평하기만 할 수는 없지.

다른 문제점은 '가독성'이 열라 떨어진다는 점이다. STL을 이중 삼중으로 사용하게 되면 작성한 놈조차 '이게 뭔 말이여?' 라는 소리를 하게 되어 있다.

즉, 템플릿의 문법 자체가 복잡한데다가 일반화 특성을 추가해버렸으니 더더욱 복잡해 보인다. 그래서 제대로 사용하기 위해서는 많은 연습이 필요하다는 점이다.

많은 사람이 STL을 꺼려하는 이유는 복잡하기 때문인데다가 이를 대체할 좋고 쉽게 이해할 수 있는 MFC가 존재하기 때문이다. 

하지만 내가 얻은 소스가 STL이라면 어쩌겠는가? 즉, 결국은 알아야 한다는 말이지... 에휴... 만약 템플릿이 무엇인지 모른다면 반드시 그것부터 이해해야 한다는 말은 꼭 하고 싶다...
posted by 대갈장군
2008. 1. 15. 05:02 프로그래밍/C++

음.. 아주 중요한 단어가 드디어 등장했다. 순수 가상 함수... Pure Virtual Function이군.

자바를 해본 사람이라면 아마도 너무나도 당연한 개념일 것이고 나처럼 C++를 구렁이 담 넘어 가듯이 알고 있는 사람이라면 왜 이말이 중요한지 아직 모를수도 있다.

C++의 큰 장점은 "다형성" 이라고 할 수 있다.

부모 클래스로 부터 차례 차례 상속을 받아 필요로 하는 함수와 멤버를 상속 받고 최종 자식 클래스는 자신이 필요한 것들을 정의하면 된다.

이때 앞에서 본 가상 함수가 필요한데, 왜 필요한가 하면은....

C프로그램을 짤때는 흔히 포인터를 선언하고 포인터를 이용해 멤버 함수를 호출하곤 한다. 이때 부모클래스의 포인터를 선언하고 자식 클래스를 대입한후 자식 클래스의 멤버 함수(부모 클래스도 같은 이름의 멤버 함수를 가지고 있다면)를 호출하고자 한다면 반드시 "동적 결합"을 해야 하고 그 동적 결합을 위해서는 반드시 부모 클래스의 멤버 함수 앞에 "Virtual"이라는 키워드를 넣어야 한다.

그렇지 않고 "정적 결합"을 하게 되면 (Virtual 키워드를 안적으면) 포인터가 선언될 당시의 타입 (부모 클래스 타입이죠)의 멤버 함수만 계속 호출하게 된다.

그래서 가상 함수라는 것이 필요로 하는데 여기서 새로 등장한 순수 가상 함수란,

아무것도 정의 되지 않은 가상 함수를 말한다... 그리고 이것을 가지는 클래스를 바로 추상 클래스 (abstract class)라한다.

그렇다면 왜 비어있는 가상 함수를 선언하는가? 바로 이 질문이 나와야 한다.

질문의 답이 바로 저 위에 써놓은 C++ 장점 때문이다. 다형성을 실현하기 위해서는 동적 결합을 해야 하는데 그렇게 하기 위해서는 서로 다른 형태를 가지는 자식 클래스들이 공통의 부모 클래스를 가질 필요가 있다.

그렇게 되어야 부모 클래스 타입의 포인터를 배열로 할당해서 자식 클래스를 각각의 배열 요소로 가지도록 한다음 임의의 자식 클래스 객체의 멤버 함수를 호출 했을때 그 클래스에 맞는 멤버 함수가 자동으로 호출되게 할 수 있다는 것이다.

개념이 쉽지는 않다. 하지만 알고나면 "아... 이게 꼭 필요하긴 하겠구나" 하는 생각이 든다.

자바를 예전에 잠깐 써봤을때 추상 클래스에 대해서 수도 없이 들었지만 사실 왜 필요한가에 대한 대답은 몰랐었다. 하지만 지금 보니 왜 추상 클래스가 필요한지 알겠다.

posted by 대갈장군
2008. 1. 10. 07:45

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

prev 1 next