반응형

http://dev.log.mumbi.net/497


1. 알고리즘( algorithm )의 일반화.
  알고리즘의 일반화라는 것은 어떠한 컨테이너( container )도 그 알고리즘을 사용할 수 있게 만드는 것이다.

이 때, 그 알고리즘은 대상 컨테이너를 순회하기 위해서 컨테이너의 반복자를 사용한다. 보통, 시작을 가리키는 반복자와, 끝을 가리키는 반복자를 사용하고 끝을 가리키는 반복자는 순회에 포함되지 않고 컨테이너의 마지막 요소 다음을 가리킨다.

여기서 중요한 사실은 STL( Standard Template Library )이 제공하는 알고리즘이 제공하는 컨테이너 뿐 아니라 기본 배열에도 사용할 수 있게 C++ 표준이 지정되었다.

특정한 형을 사용하는 알고리즘을 일반화 시켜보면..

1. int 형 배열의 모든 원소의 합을 구하여 반환하는 함수.
 int Sum( int* _pFirst, int* _pLast )
{
    int sum = *_pFirst++;
 
    while( _pFirst != _pLast )
        sum += *_pFirst++;
 
    return sum;
}

배열의 첫 요소의 주소와 마지막 요소의 다음 주소를 인자로 받아 각 요소들을 순회하면서 합을 구한다.

2. 배열을 비롯한 모든 컨테이너에서 사용하기 위해 템플릿( template )을 적용한 일반화된 함수.
 template< class IteratorType >
 Sum( IteratorType _first, IteratorType _last )
{
    ???? sum = *_first++;
 
    while( _first != _last )
        sum += *_first++;
 
    return sum;
}

컨테이너에서는 포인터( pointer )를 일반화한 반복자( iterator )를 사용하기 때문에 시작을 가리키는 반복자와 끝을 가리키는 반복자를 인수로 받아 각 요소들을 순회하면서 합을 구한다.

그런데 요소들의 타입( type )을 모르기 때문에 그 합의 타입도 알 수 없다.

3. 반복자가 가리키는 타입 알려주는 value_type으로 일반화한 함수.
 template< class IteratorType >
typename IteratorType::value_type Sum( IteratorType _first, IteratorType _last )
{
    typename IteratorType::value_type sum = *_first++;
 
    while( _first != _last )
        sum += *_first++;
 
    return sum;
}

컨테이너의 반복자는 value_type이라는 타입을 제공한다. 반복자의 value_type은 반복자가 가리키는요소의 타입이다.

하지만 컨테이너의 반복자는 value_type을 제공하지만 기존의 포인터는 객체가 아니기 때문에 value_type을 제공하지 않으므로 배열에 대해서는 일반화되지 않았다.

배열의 포인터로 어떻게 요소의 타입을 알 수 있을까?

2. 템플릿 부분 특수화( partial specialization ).
배열의 요소 타입을 알아 내기 위해 템플릿의 부분 특수화를 알아야 한다.

[C++] 템플릿 특수화 ( template specialization )

STL 은 부분 특수화를 통해 iterator_traits 를 제공한다.

 template<class _iter="">
    struct iterator_traits      // 포인터가 아닐 경우( 반복자 ).
    {   // get traits from iterator _Iter
    typedef typename _Iter::iterator_category iterator_category;
    typedef typename _Iter::value_type value_type;      // <--
    typedef typename _Iter::difference_type difference_type;
    typedef difference_type distance_type;  // retained
    typedef typename _Iter::pointer pointer;
    typedef typename _Iter::reference reference;
    };
 
template<class _ty="">
    struct iterator_traits<_Ty *> // 포인터일 경우.
    {   // get traits from pointer
    typedef random_access_iterator_tag iterator_category;
    typedef _Ty value_type;         // <--
    typedef ptrdiff_t difference_type;
    typedef ptrdiff_t distance_type;    // retained
    typedef _Ty *pointer;
    typedef _Ty& reference;
    };

 

위의 코드는 MSVC9 에서 제공하는 STL의 iterator_traits 이다.

부분 특수화를 이용해 포인터일 경우와 반복자일 경우의 value_type을 다르게 하여 제공한다.

3. iterator_traits.
결론적으로 iterator_traits 는 반복자 특질이라고 번역되지만 사실, 반복자와 배열의 포인터를 구분하기 위해서 만들어진 것이다.

알고리즘을 구현하고 있다면 iterator_traits 가 무엇인지 반드시 알아야 한다.


iterator_traits Class 

Visual Studio 2005

A template helper class used to specify all the critical type definitions that an iterator should have.

template<class Iterator>
   struct iterator_traits {
   typedef typename Iterator::iterator_category iterator_category;
   typedef typename Iterator::value_type value_type;
   typedef typename Iterator::difference_type difference_type;
   typedef typename Iterator::pointer pointer;
   typedef typename Iterator::reference reference;
   };
template<class Type>
   struct iterator_traits<Type*> {
   typedef random_access_iterator_tag iterator_category;
   typedef Type value_type;
   typedef ptrdiff_t difference_type;
   typedef Type *pointer;
   typedef Type& reference;
   };
template<class Type>
   struct iterator_traits<const Type*> {
   typedef random_access_iterator_tag iterator_category;
   typedef Type value_type;
   typedef ptrdiff_t difference_type;
   typedef const Type *pointer;
   typedef const Type& reference;
   };

The template class defines the member types

  • iterator_category: a synonym for Iterator::iterator_category.

  • value_type: a synonym for Iterator::value_type.

  • difference_type: a synonym for Iterator::difference_type.

  • pointer: a synonym for Iterator::pointer.

  • reference: a synonym for Iterator::reference.

The partial specializations determine the critical types associated with an object pointer of type Type * or const Type *.

In this implementation you can also use several template functions that do not make use of partial specialization:

template<class Category, class Type, class Diff>
C _Iter_cat(const iterator<Category, Ty, Diff>&);
template<class Ty>
    random_access_iterator_tag _Iter_cat(const Ty *);

template<class Category, class Ty, class Diff>
Ty *_Val_type(const iterator<Category, Ty, Diff>&);
template<class Ty>
    Ty *_Val_type(const Ty *);

template<class Category, class Ty, class Diff>
Diff *_Dist_type(const iterator<Category, Ty, Diff>&);
template<class Ty>
    ptrdiff_t *_Dist_type(const Ty *);

which determine several of the same types more indirectly. You use these functions as arguments on a function call. Their sole purpose is to supply a useful template class parameter to the called function.

// iterator_traits.cpp
// compile with: /EHsc
#include <iostream>
#include <iterator>
#include <vector>
#include <list>

using namespace std;

template< class it >
void
function( it i1, it i2 )
{
   iterator_traits<it>::iterator_category cat;
   cout << typeid( cat ).name( ) << endl;
   while ( i1 != i2 )
   {
      iterator_traits<it>::value_type x;
      x = *i1;
      cout << x << " ";
      i1++;
   };   
   cout << endl;
};

int main( ) 
{
   vector<char> vc( 10,'a' );
   list<int> li( 10 );
   function( vc.begin( ), vc.end( ) );
   function( li.begin( ), li.end( ) );
}

Output

struct std::random_access_iterator_tag
a a a a a a a a a a 
struct std::bidirectional_iterator_tag
0 0 0 0 0 0 0 0 0 0 

Header: <iterator>

반응형
반응형

http://blog.daum.net/coolprogramming/93

 

4, 삽입 반복자

삽입 반복자(insert iterator)는 알고리즘에서 반복자가 가리키는 위치에 값을 "덮어"(overwrite)쓰지 않고 "삽입"(insert)할 수 있도록 바꿔주는 반복자입니다.

STL은 3 가지의 삽입 반복자를 제공합니다.

  • back_insert_iterator : push_back 멤버 함수를 사용하여 삽입

    • 시퀀스 컨테이너(string, vector, list, deque)는 push_back 멤버 함수를 제공하므로 사용 가능
  • front_insert_iterator : push_front 멤버 함수를 사용하여 삽입

    • list와 deque만 push_front 멤버 함수를 제공하므로 사용 가능
  • insert_iterator : insert 멤버 함수를 사용하여 삽입

    • 모든 컨테이너는 insert 멤버 함수를 제공하므로 사용 가능

반복자를 이용한 덮어쓰기와 삽입을 그림으로 보입니다.

1) 덮어쓰기 모드

insert_iterator(1).png

2) 삽입 모드

insert_iterator2.png

일반적으로 알고리즘은 덥어쓰기로 동작하므로 알고리즘을 사용하기 전에 저장할 원소의 size가 존재해야 합니다.

간단한 copy 알고리즘을 예로 사용합니다.

#include <iostream>

#include <algorithm>
#include <vector>
using namespace std;

void main( )
{
vector<int> v1;
vector<intv2(3);


v1.push_back(10);

v1.push_back(20);
v1.push_back(30);

copy(v1.begin(), v1.end(), v2.begin());

forint i = 0 ; i < v2.size() ; i++)
cout << v2[i] << " ";
cout << endl;
}

  1. 10 20 30

v1.begin()~v2.end()까지의 원소를 모두 v2에 저장합니다. 이때 v2는 저장할 개수만큼의 size를 가지고 있어야합니다. 여기서는 3개입니다.

만약 size를 가지고 있지 않다면 실행 오류가 발생합니다.

v2의 size는 0 입니다.

#include <iostream>

#include <algorithm>
#include <vector>
using namespace std;

void main( )
{
vector<int> v1;
vector<intv2;


v1.push_back(10);

v1.push_back(20);
v1.push_back(30);

copy(v1.begin(), v1.end(), v2.begin());

forint i = 0 ; i < v2.size() ; i++)
cout << v2[i] << " ";
cout << endl;
}

  1. 안녕~~!

v2의 size가 0이므로 copy 알고리즘에서 실행 오류가 발생합니다.

이것은 해결하려면 copy 알고리즘에 삽입 반복자를 전달하면 됩니다.

insert_iterator를 사용한 예제입니다.

#include <iostream>

#include <algorithm>
#include <vector>
using namespace std;

void main( )
{
vector<int> v1;
vector<int> v2;


v1.push_back(10);

v1.push_back(20);
v1.push_back(30);

copy(v1.begin(), v1.end(), inserter< vector<int> >(v2, v2.begin()) );

forint i = 0 ; i < v2.size() ; i++)
cout << v2[i] << " ";
cout << endl;
}

  1. 10 20 30

inserter는 insert_iterator를 생성합니다. 이 삽입 반복자는 insert() 멤버 함수를 사용하여 삽입니다.

inserter는 템플릿 함수로 insert_iterator를 반환합니다. 첫 번째 인자에 컨테이너를 두 번째 인자에 삽입할 위치의 반복자를 지정하면 됩니다.

또 다른 예제입니다.

#include <iostream>

#include <algorithm>
#include <vector>
using namespace std;

void main( )
{
vector<int> v1;
vector<int> v2;


v1.push_back(10);

v1.push_back(20);
v1.push_back(30);


v2.push_back(100);

v2.push_back(200);
v2.push_back(300);

copy(v1.begin(), v1.end(), inserter< vector<int> >(v2, v2.begin() ) );

forint i = 0 ; i < v2.size() ; i++)
cout << v2[i] << " ";
cout << endl;
}

  1. 10 20 30 100 200 300

v2가 3개의 원소(100,200,300)를 가지고 있고 v2.begin()에 삽입 위치를 지정했으므로 앞쪽에 삽입됩니다.

back_insert_iterator를 사용한 예제입니다.

#include <iostream>

#include <algorithm>
#include <vector>
using namespace std;

void main( )
{
vector<int> v1;
vector<int> v2;

v1.push_back(10);

v1.push_back(20);
v1.push_back(30);

v2.push_back(100);

v2.push_back(200);
v2.push_back(300);

copy(v1.begin(), v1.end(), back_inserter< vector<int> >(v2) );

forint i = 0 ; i < v2.size() ; i++)
cout << v2[i] << " ";
cout << endl;
}

  1. 100 200 300 10 20 30

back_inserter는 back_insert_iterator를 생성합니다. 이 삽입 반복자는 push_back() 멤버 함수를 사용하여 삽입(추가)합니다.

back_inserter는 템플릿 함수로 back_insert_iterator를 반환합니다. 함수의 인자에 컨테이너 지정하면 됩니다.

vector는 push_front 멤버 함수가 없으므로 front_insert_iterator는 사용할 수 없습니다.

#include <iostream>

#include <algorithm>
#include <vector>
using namespace std;

void main( )
{
vector<int> v1;
vector<int> v2;

v1.push_back(10);

v1.push_back(20);
v1.push_back(30);

v2.push_back(100);

v2.push_back(200);
v2.push_back(300);

copy(v1.begin(), v1.end(), front_inserter< vector<int> >(v2) );

forint i = 0 ; i < v2.size() ; i++)
cout << v2[i] << " ";
cout << endl;
}

  1. 안녕~~!

에러입니다. front_inserter는 템플릿 함수로 front_insert_iterator를 반환하지만 vector는 push_front() 멤버 함수가 없으므로 front_insert_iterator를 사용할 수 없습니다.

list는 push_front 멤버 함수를 가지고 있으므로 front_insert_iterator를 사용할 수 있습니다.

#include <iostream>
#include <algorithm>
#include <list>
using namespace std;

void main( )
{
list<int> lt1;
list<int> lt2;

lt1.push_back(10);
lt1.push_back(20);
lt1.push_back(30);

lt2.push_back(100);

lt2.push_back(200);
lt2.push_back(300);

copy(lt1.begin(), lt1.end(), front_inserter< list<int> >(lt2) );

list<int>::iterator iter;
for( iter = lt2.begin(); iter != lt2.end() ; iter++)
cout << *iter << " ";
cout << endl;
}

  1. 30 20 10 100 200 300

push_front 멤버 함수를 사용하여 정수를 삽입합니다.

5, 입,출력 스트림 반복자

입,출력 스트림 반복자(input,output stream iterator)는 I/O 스트림에 연결된 조금 특별한 반복자입니다. 알고리즘에서 스트림 입출력을 쉽게 수행할 수 있도록 제공합니다. 입력 스트림 반복자는 istream_iterator 클래스를 출력 스트림 반복자는 ostream_iterator 클래스를 사용합니다.

copy 알고리즘에서 출력 스트림 반복자를 사용한 예제입니다.

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

void main( )
{
vector<int> v;

v.push_back(10);
v.push_back(20);
v.push_back(30);
v.push_back(40);
v.push_back(50);

copy(v.begin(), v.end(), ostream_iteratorint >(cout, " " ) );
cout<< endl;

copy(v.begin(), v.end(), ostream_iteratorint >(cout, ", " ) );
cout<< endl;

}

  1. 10 20 30 40 50
    10, 20, 30, 40, 50,

ostream_iterator는 템플릿 클래스로 출력 반복자를 추상화한 클래스입니다. 템플릿 인자는 출력 타입이며(int) 생성자의 첫 번째 인자는 출력 스트림 객체, 두 번째 인자는 출력 값들 사이에 출력될 문자열입니다.

다음은 istream_iterator의 간단한 예제입니다.

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;


void main( )
{
vector<int> v;

v.push_back(10);
v.push_back(20);
v.push_back(30);
v.push_back(40);
v.push_back(50);

copy( istream_iterator<int>(cin), istream_iterator<int>(), ostream_iterator< int >(cout, "," ) );
cout<< endl;
}

  1. 10 20 30 40 50^Z
    10,20,30,40,50,

예제는 입력 스트림 객체(cin)에서 입력되는 정수들을 스트림 끝까지 입력 받아 출력하는 예제입니다.

istream_iterator<int>( cin )은 cin(입력 스트림 객체)을 통해 입력되는 정수 값을 입력 받는 반복자를 istream_iterator<int>( )은 입력 스트림 반복자의 끝 표시기(end marker)를 역할을 하는 입력 반복자를 의미합니다.

여기까지 C++, STL QS1을 모두 마무리합니다. ㅠ.ㅠ...

10개월 동안 해오던 일을 마무리하니 시원함? 아쉬움? 뿌듯함? 허탈함?이 모두 공존합니다.

아직 못 다한 고급(?)적인 내용은 C++, STL QS2에서 공부합니다. QS2는 조금 어려운 내용이 될 수도...

수고하셨습니다. 꾸벅!

반응형
반응형

http://cmlife.blogspot.com/2007_02_01_archive.html


auto_ptr

책: C++ Standard Library 튜토리얼.레퍼런스
(니콜라이 M.조슈티스 저 / 문정환.김희준 공역 )
68p
auto_ptr 클래스

일반적으로 사용자는 포인터를 사용할 경우, 명시적으로 리소스를 관리해 주어야 한다.
void foo()
{
classA * ptr = new ClassA; // 생성..
... 작업수행..
delete ptr; // 파괴
}
이 함수에는 문제가 발생할 여지가 있다.
객체를 파괴하는 작업을 잊을수도 있고..
예외가 발생할 경우에는 delete문장이 호출되지 않고 곧바로 함수가 종료되어..메모리 릭이 발생할 수 있다.
그래서 예외처리를 위해서 try {... } catch() {....}로 처리해야 한다
만약 여러개의 객체일 경우 더욱 복잡해져 버리게 된다.
스마트 포인터는 이런 경우에 도움이 될수 있다. 스마트 포인터는 포인터 자체가 파괴될때 가리키고 있던 리소스를 항상 해제할 수 있다. 게다가 정상적이던 예외로이던 함수가 종료될때 포인터는 자동으로 파괴된다.

70p
auto_ptr은 자신이 가리키는 객체에 대한 소유자로서 행동하는 포인터이다. 결과적으로 auto_ptr이 파괴되면 소유하던 객체도 자동으로 파괴되는 것이다. auto_ptr의 요구사항은, 객체는 단 하나의 소유자만을 가질수 있다는 것이다.

#include // auto_ptr을 사용하기 위한 헤더
using namespace std;

void foo()
{
auto_ptr ptr( new ClassA );
......작업수행..
}

delete와 예외처리를 위한 문장이 더이상 필요하지 않다.
auto_ptr은 통상적인 포인터와 같은 인터페이스를 가지고 있다.
'*'연산자와, '->"연산자를 사용할수 있으며 클래스나 구조체일 경우 멤버에 대한 억세스를 제공한다.
그러나 ++/--같은 포인터 증감 연산은 사용불가하다..(Undefined)

auto_ptr<>클래스는 대입연산자로(=)로 초기화할 수 없다.
반드시 생성자를 사용하여 초기화를 해야 한다.

auto_ptr ptr1( new ClassA ); // OK!!
auto_ptr ptr2 = new ClassA; // ERROR !!!!!!!!!!!!!!

또는
auto_ptr ptr3; // auto_ptr이 생성된다.
ptr3 = auto_ptr(new ClassA); // 생성자를 이용해 명시적으로 생성한다.
ptr3이 가진 소유권이 파괴된후.. 새로 생성한 ( ...= auto_ptr(new ClassA) ) 것으로 소유권을 이전 받는다.

71p
auto_ptr에 의한 소유권 이전

auto_ptr ptr1( new ClassA );
auto_ptr ptr2( ptr1 );


이 경우에.. ptr1이 가지고 있던 소유권이 ptr2로 이전되고..
ptr1은 NULL 포인터를 가지게 된다...
그래서 ptr1은 이제 더이상 사용되서는 안된다.(이것은 프로그래머의 책임.)

auto_ptr ptr1( new ClassA );
auto_ptr ptr2;
ptr2 = ptr1;
이 경우에도 ptr1의 소유권이 ptr2로 넘어간다.


auto_ptr ptr1( new ClassA );
auto_ptr ptr2( new ClassA );
ptr2 = ptr1;

ptr1, ptr2모두 각각 다른 객체에 대한 소유권을 가지고 있을때는 어떻게 처리될까..
이때는 ptr2 = ptr1시에... ptr2가 가진 객체의 소유권이 파괴가 된다..
그런후 ptr1의 소유권을 이전받는 것이다.

위와 같은 소유권의 이전은 함수에서도 똑같이 적용된다.
auto_ptr foo()
{
auto_ptr ptr
....
return ptr;
}

void g()
{
auto_ptr p;

p = foo(); // 소유권을 이전 받는다.
}


auto_ptr을 인자로 사용할 경우 문제가 생긴다.

auto_ptr p( new int );

*p = 42; // *p에 값을 대입할수 있다. 정상.
bad_print( p ); // 소유권 이전이 일어난다.
// p가 가진 소유권이 bad_print()함수로 넘어가고 p는 NULL이 되어버렸다.
*p = 34; // 런타임 에러가 발생한다.

그럼 레퍼런스를 사용하면 되지 않을까 생각할수 있지만..
레퍼런스로 전달하면 함수는 소유권을 이전받았을 수도 있고 아닐 수도 있다.
auto_ptr을 레퍼런스타입으로 전달하는 것은 반드시 피해야 한다.

auto_ptr을 좀더 안전하게 만드는 방법으로 결정된 것은..const를 붙이는 것이다.
const auto_ptr p( new int );
*p = 42;
bad_print( p ); // 컴파일 에러.
*p = 34;
return p; // 컴파일 에러

const auto_ptr을 사용하면 의도하지 않은 소유권의 이전을 막을수 있다.
const는 auto_ptr객체의 값을 변경할수 없다는 뜻이 아니다...,
소유권을 이전할수 없다는 것을 의미한다.


(76p)
멤버 변수로의 auto_ptr
클래스 안에서 auto_ptr을 사용하게 되면 리소스 릭을 피할수 있다. 또한 파괴자가 필요없게 된다.
객체의 생성시 예외가 발생해도 리소스 릭을 피할수 있는 것이다.
한가지 주목해야 할 점은 파괴자는 생략하더라도, 복사 생성자와 대입연산자는 반드시 작성해야 한다.


(78p)
잘못된 사용례.

auto_ptr은 배열을 참조하는 것을 허락하지 않는다. 이것은 auto_ptr이 자신이 소유한 객체를 파괴하기 위해서 delete[]가 아닌 delete를 호출하기 때문이다. C++표준 라이브러리에서는 배열타입을 위한 auto_ptr은 없다. 대신 라이브러리는 데이터의 집합을 관리하기 위해서 컨테이너 클래스들을 제공한다.

auto_ptr은 표준 컨테이너 클래스와 함께 사용될 수 없다.
auto_ptr은 값의 복사가 아닌 소유권의 이전이 이루어지므로 그렇다.
다행히 라이브러리는 이러한 실수에 대해 컴파일에러를 제공한다. :)

(79p)
const auto_ptr 사용예..

#include #include 
template ostream & operator<<( ostream &os, const auto_ptr & p ){ if(p.get()==NULL) { os << "NULL"; } else { os << *p; } return os; } templatevoid Func( const auto_ptr & p ){ *p += 1;
cout << "Func(+1시킴) : " <<> p (new int(42)); auto_ptr q;

cout << "after initialization:" << endl; cout << "p : " << p << endl; cout << "q : " << q << endl;
Func(p);
q = p; cout << "\nafter assign:" << endl; cout << "p : " << p << endl; cout << "q : " << q << endl;
*q += 13; p = q; cout << "\nafter change:" << endl; cout << "p : " << p << endl; cout << "q : " << q << endl;
cin.get();
}

반응형
반응형


 http://blog.naver.com/uleena?Redirect=Log&logNo=80042072372




#include <iostream>

#include <string>

#include <functional>

using namespace std;

 

#include <ctime>

#include <bitset>

 

// unique 한랜덤을만들어내는함수객체

template<int MAX> class URandom

{

        bitset<MAX> bs;

        bool recycle;

public:

        URandom(bool b = false) : recycle(b)

        {

               srand(time(0)); // 난수초기화

               bs.set();       // 모든비트를1

        }

 

        int operator()()

        {

               if ( bs.none() )   // 모두0인경우

               {

                       if ( recycle == false// 재사용안하는경우

                              return -1;

                       else

                              bs.set();  // 모두1

               }

 

               int n = -1;

               while ( !bs.test( n = (rand() % MAX) ));

               bs.reset(n);

               return n;

        }

};

 

int main()

{

        URandom<5> r;

 

        for ( int i = 0; i < 15;++i)

               cout << r() << endl;

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

// 3. bitset : 비트의집합

#include <bitset>

 

int main()

{

        bitset<10> bs;

        bs.reset(); // 모두0

 

        // 각비트를조작하는법

        bs.set(4);

        bs[5] = true;

        bs[6].flip();

 

        // 비트조사

        if ( bs.test(4) ) cout << "비트4set" << endl;

        if ( bs[5]      ) cout << "비트5set" << endl;

 

        unsigned long n = bs.to_ulong();

        cout << n << endl;

 

        string s = bs.to_string();

        cout << s << endl;

}

 

 

       

 

 

 

 

// 2. set : 순서를가지고요소를저장한다. (대부분의STL구현물은RB-Tree사용)

 

#include <set>

 

int main()

{

        typedef set<int, greater<int>> SET;

 

        //set< int, greater<int> > s;

        SET s;

       

        s.insert(10);

        s.insert( 5);

        s.insert(15);

        s.insert(30);

        s.insert(4);

 

        pair< SET::iterator, bool> ret = s.insert(10); // 중복허용될까?

 

        if ( ret.second == false )

               cout << "이미요소가있습니다." << endl;

 

 

        ostream_iterator<int> os(cout, " ");

 

        copy( s.begin(), s.end(), os);

}

 

 

 

 

 

 

 

 

 

 

// 1. pair의개념

 

 

template<typename T1, typename T2> struct pair

{

        T1 first;

        T2 second;

 

        pair(T1 a = T1(), T2 b = T2() ) :first(a), second(b) {}

};

 

pair<intint> foo()

{

        // 이안에서2개의값을리턴해주고싶다.

}

 

 

int main()

{

        // TR1(또는boost)에는pair에개선된버전인tuple이있다.

        tuple<> none;

        tuple<int> one;

        tuple<intint> two;

        tuple<intintint> three;

        tuple<intintintint> n;

 

 

 

        pair<int, pair<intint>> p2;

 

        pair<intdouble> p1(1,3.4);

 

        cout << p1.second << endl; // ?

}

반응형
반응형

typename 을 써야 하는 순간

 

http://blog.naver.com/spinx85?Redirect=Log&logNo=140112304004

 

 

 

 

http://dev.log.mumbi.net/497

[C++] iterator_traits( 반복자 특질 )이란 무엇인가?

Language/C++ | 2010/01/28 16:56 | Posted by 임준환( 멈비 )

1. 알고리즘( algorithm )의 일반화.

  알고리즘의 일반화라는 것은 어떠한 컨테이너( container )도 그 알고리즘을 사용할 수 있게 만드는 것이다.

이 때, 그 알고리즘은 대상 컨테이너를 순회하기 위해서 컨테이너의 반복자를 사용한다. 보통, 시작을 가리키는 반복자와, 끝을 가리키는 반복자를 사용하고 끝을 가리키는 반복자는 순회에 포함되지 않고 컨테이너의 마지막 요소 다음을 가리킨다.

여기서 중요한 사실은 STL( Standard Template Library )이 제공하는 알고리즘이 제공하는 컨테이너 뿐 아니라 기본 배열에도 사용할 수 있게 C++ 표준이 지정되었다.

특정한 형을 사용하는 알고리즘을 일반화 시켜보면..

1. int 형 배열의 모든 원소의 합을 구하여 반환하는 함수.

1
2
3
4
5
6
7
8
9
int Sum( int* _pFirst, int* _pLast )
{
    int sum = *_pFirst++;
  
    while( _pFirst != _pLast )
        sum += *_pFirst++;
  
    return sum;
}

배열의 첫 요소의 주소와 마지막 요소의 다음 주소를 인자로 받아 각 요소들을 순회하면서 합을 구한다.

2. 배열을 비롯한 모든 컨테이너에서 사용하기 위해 템플릿( template )을 적용한 일반화된 함수.

1
2
3
4
5
6
7
8
9
10
templateclass IteratorType >
???? Sum( IteratorType _first, IteratorType _last )
{
    ???? sum = *_first++;
  
    while( _first != _last )
        sum += *_first++;
  
    return sum;
}

컨테이너에서는 포인터( pointer )를 일반화한 반복자( iterator )를 사용하기 때문에 시작을 가리키는 반복자와 끝을 가리키는 반복자를 인수로 받아 각 요소들을 순회하면서 합을 구한다.

그런데 요소들의 타입( type )을 모르기 때문에 그 합의 타입도 알 수 없다.

3. 반복자가 가리키는 타입 알려주는 value_type으로 일반화한 함수.

1
2
3
4
5
6
7
8
9
10
templateclass IteratorType >
typename IteratorType::value_type Sum( IteratorType _first, IteratorType _last )
{
    typename IteratorType::value_type sum = *_first++;
  
    while( _first != _last )
        sum += *_first++;
  
    return sum;
}

컨테이너의 반복자는 value_type이라는 타입을 제공한다. 반복자의 value_type은 반복자가 가리키는 요소의 타입이다.

하지만 컨테이너의 반복자는 value_type을 제공하지만 기존의 포인터는 객체가 아니기 때문에 value_type을 제공하지 않으므로 배열에 대해서는 일반화되지 않았다.

배열의 포인터로 어떻게 요소의 타입을 알 수 있을까?

2. 템플릿 부분 특수화( partial specialization ).

배열의 요소 타입을 알아 내기 위해 템플릿의 부분 특수화를 알아야 한다.

[C++] 템플릿 특수화 ( template specialization ) 

STL 은 부분 특수화를 통해 iterator_traits 를 제공한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template<CLASS _iter="">
    struct iterator_traits      // 포인터가 아닐 경우( 반복자 ).
    {   // get traits from iterator _Iter
    typedef typename _Iter::iterator_category iterator_category;
    typedef typename _Iter::value_type value_type;      // <--
    typedef typename _Iter::difference_type difference_type;
    typedef difference_type distance_type;  // retained
    typedef typename _Iter::pointer pointer;
    typedef typename _Iter::reference reference;
    };
  
template<CLASS _ty="">
    struct iterator_traits<_Ty *> // 포인터일 경우.
    {   // get traits from pointer
    typedef random_access_iterator_tag iterator_category;
    typedef _Ty value_type;         // <--
    typedef ptrdiff_t difference_type;
    typedef ptrdiff_t distance_type;    // retained
    typedef _Ty *pointer;
    typedef _Ty& reference;
    };</CLASS></CLASS>

위의 코드는 MSVC9 에서 제공하는 STL의 iterator_traits 이다.

부분 특수화를 이용해 포인터일 경우와 반복자일 경우의 value_type을 다르게 하여 제공한다.

3. iterator_traits.

결론적으로 iterator_traits 는 반복자 특질이라고 번역되지만 사실, 반복자와 배열의 포인터를 구분하기 위해서 만들어진 것이다.

알고리즘을 구현하고 있다면 iterator_traits 가 무엇인지 반드시 알아야 한다.

반응형
반응형


http://www.viper.pe.kr/cgi-bin/moin.cgi/STL/%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88

1. 컨테이너(Container) [Bottom] [Top]

1.1. 컨테이너의 특징 [Bottom] [Top]

  • 특징

    vector

    deque

    list

    set/multiset

    map/multimap

    내부 자료구조

    동적 배열

    배열의 배열

    이중 링크드 리스트

    바이너리 트리

    바이너리 트리

    원소

    value

    value

    value

    value

    Key/value

    중복 허옹 여부

    허용

    허용

    허용

    허용 안함 / 허용

    key에 대해서만 허용 안함 / 허용

    랜덤 액세스 여부

    허용

    허용

    허용 안함

    허용 안함

    key를 사용하여 가능 / 허용 안함

    반복자 카테고리

    랜덤 액세스

    랜덤 액세스

    양방향

    양방향(원소는 상수이다)

    양방향(key는 상수)

    원소의 검색/찾기

    느림

    느림

    매우 느림

    빠름

    key에 대해서 빠름

    원소의 삽입 및 제거가 
    빠른 구간

    컨테이너의 끝

    컨테이너의 앞과 끝

    아무곳에서나

    -

    -

    삽입/제거 동작의 반복자와 
    레퍼런스, 포인트의 무효화

    재할당시

    항상

    일어나지 않음

    일어나지 않음

    일어나지 않음

    제거된 원소에 대한 메모리 
    해제 여부

    일어나지 않음

    가끔 일어남

    항상 일어남

    항상 일어남

    항상 일어남

    메모리 예약의 허용 여부

    허용

    허용 안함

    -

    -

    -

    트랜젝션 안전성(성공하거나 
    아무런 영향도 없다)

    push, pop

    push, pop

    sort()와 할당 연산자를 
    제외한 모든 연산들

    다중 원소 삽입 제외한 
    모든 연산

    다중 원소 삽입 제외한 
    모든 연산

1.2. 시퀀스 컨테이너(Sequence container) [Bottom] [Top]

  • vector - 가변 길이 시퀀스, 임의 접근(random access) 가능, 시퀀스의 끝에서 삽입/삭제.
  • deque - 가변 길이 시퀀스, 임의 접근 가능, 시퀀스의 앞, 끝에서 삽입/삭제.
  • list - 가변 길이 시퀀스, 선형 시간 접근만 가능, 임의의 위치에 삽입/삭제 가능.

1.3. 정렬 연관 컨테이너(Sorted associative container) [Bottom] [Top]

  • set - 유일 키(Unique key) 지원, 빠른 키(Key) 값 검색.
  • multiset - 중복 키(Duplicate key) 지원, 빠른 키 값 검색.
  • map - 유일 키 지원, 키 값으로 데이터 검색.
  • multimap - 중복 키 지원, 키 값으로 데이터 검색.

1.4. 컨테이너 어댑터(Container adaptor) [Bottom] [Top]

  • stack - vector, deque, list 에 적용 가능, LIFO(후입선출) 구조.
  • queue - deque, list 에 적용 가능, FIFO(선입선출) 구조.
  • priority_queue - vector, deque 에 적용 가능, 비교 함수 객체에 따라 우선 순위 처리.

1.5. 비표준 컨테이너(Non-standard container) [Bottom] [Top]

  • hash_map
  • hash_set
  • bitset

2. 컨테이너 동작 [Bottom] [Top]

주요 컨테이너의 기본 동작에 대하여 그림으로 정리

참조> 그림을 보는 방법

  • 구분

    설명

    초록색 (Green)

    컨테이너

    파란색 (Blue)

    참조 함수

    보라색 (Purple)

    삽입/삭제 함수

2.1. vector 동작 [Bottom] [Top]

2.2. queue 동작 [Bottom] [Top]

2.3. deque, list 동작 [Bottom] [Top]

2.4. stack 동작 [Bottom] [Top]

3. 컨테이너 함수 [Bottom] [Top]

3.1. insert() 함수 [Bottom] [Top]

  • Toggle line numbers
       1 // 클래스 선언
       2 class CData
       3 {
       4         ...
       5 };
       6 
       7 // 데이터형 선언
       8 typedef std::set< CData * >                     SET_DATA;
       9 typedef std::pair< SET_DATA::iterator, bool >   SET_DATA_INSERT_RESULT;
      10 
      11 
      12 //------------------------------------------------------------------------------
      13 // 컨테이너 정의
      14 SET_DATA setData;
      15 
      16 // 새로운 객체 할당
      17 CData * pData = new CData();
      18 
      19 // 데이터 삽입 & 결과
      20 SET_DATA_INSERT_RESULT result = setData.insert( pData );
      21 if( false == result.second )
      22 {
      23         // ERROR: 동일한 데이타 존재함
      24 }
    


UnfinishedPage

반응형
반응형

http://cpu815.blog.me/110035113430


 Balanced Binary Search Tree(ex : Red-Black Tree) 
역시 매우 중요한 자료구조균형 잡힌(!) 이진 탐색 트리는 추가삭제검색을 모두 로그 시간 O(logn)에 수행할 수 있는 매우 우수한 자료구조이다비록 해쉬 테이블 상수 시간에 이루어지지는 않지만,탐색 트리의 장점은 키 값들이 정렬이 되어있다는 것이다따라서 위의 전화번호부 예에서 사람 이름에 대해서 소팅을 많이 해야 한다면이진 트리를 사용해야 한다.

이진 트리는 아래와 같이 생긴 자료구조이다그러나 최악의 경우 모든 원소가 한 쪽에만 달려있다면,이는 리스트와 전혀 다름이 없다.

 

 

 

 

 

 

따라서양쪽 노드의 개수를 "적절히분배를 해주는 것이 가장 중요하다만약 똑같이 분배가 되어있다면탐색 및 연산 작업이 로그 시간에 수행될 수 있다최대 트리의 높이만큼 순회하면 되므로 log 시간이 걸리는 것이다이렇게 연산 시간을 로그 시간이 될 수 있도록 보장하는 이진 트리를 Balanced Binary Search Tree라고 부른다여기에는 정말로 많은 자료구조가 있으며 해야 할 얘기도 무척 많다그러나 대표적인 Red-Black Tree만 얘기해보자.

 

 

 

 

그림에서 보듯이각각의 노드에 빨강검정색의 속성을 추가한다그리고 빨간 노드의 자식은 모두 검정 노드여야 한다와 같은 여러 속성을 정하고삽입/추가할 때 마다 이 속성이 유지되도록 보정을 해준다. (: rotation) 그래서 노드가 균형을 유지하도록 한다Red-Black Tree는 양쪽 노드의 깊이가 서로 두배 이상 차이가 나지 않음을 보장한다

반면AVL Tree라고 불리는 녀석은 양쪽 노드의 길이가 최대 하나 밖에 차이가 나지 않도록 한다.훨씬 더 엄격하게 균형을 맞춘다따라서 당연히 이 균형을 맞추는 보정 작업에 소요되는 시간이 Red-Black Tree보다 크다.

키와 데이터 쌍을 저장할 때검색이 가장 우선 순위라면 당연히 해쉬 테이블을 사용해야 할 것이다.그러나 이 키 값들이 정렬이 되고 싶다면 Red-Black Tree가 가장 좋은 자료구조이다.
 


정리

형태

순서여부

색인여부

삽입/삭제 

검색속도

중복여부

List

Yes

No

Fast (constant time)

Slow O(n)

Yes

Array

Yes

By int (constant time)

Slow O(n) except if inserting at end, in which case constant time

Slow O(n)

Yes

(Hash)Map

No

By key (constant time)

Fast (constant time)

Fast (constant time)

No (keys) Yes (values)

Red-Black

Yes

By key O(log n)

Fast O(log n)

Fast O(log n)

No

 

[출처] Red-Black Tree|작성자 카푸치노

반응형
반응형

http://blog.naver.com/infopub?Redirect=Log&logNo=100024418644

반응형
반응형

http://blog.daum.net/coolprogramming/90

 1, 함수자

operator() 연산자를 중복하고 있는 클래스의 객체를 함수 객체( function object) 혹은 함수자(functor)라 합니다.

그래서 함수자는 함수처럼 사용할 수 있습니다.

함수자는 02. C++, STL QS2의 함수 포인터와 함수자를 참고하세요.

 

아래는 간단한 세가지 함수류(함수, 함수 포인터, 함수자)를 사용한 예제 코드입니다.

 

1 함수, 2 함수 포인터, 3 함수자를 사용한  두 정수의 합 예제입니다.

#include <iostream>
using namespace std;

int Plus(int a, int b)
{
    return a+b;
}
struct CPlus
{
    int operator( )(int a, int b)
    {
        return a+b;
    }
};
void main( )
{
    int result;
    int (*pPlus)(intint) = Plus;
    CPlus oPlus;

    result = Plus( 10 , 20)// 1. 함수 호출
    cout << result << endl;
   
    result = pPlus( 10, 20)// 2. 함수 포인터로 호출
    cout << result << endl;

    result = oPlus( 10, 20)// 3. 함수자로 호출
    cout << result << endl;
}
  1. 30
    30
    30

 위 예제를 보면 함수, 함수 포인터, 함수자의 인터페이스는 모두 같다는 것을 알 수 있습니다.

 

함수자를 사용하면 아래와 같은 장점이 있습니다.

  • 컴파일 시에 함수자를 알고리즘의 인자로 전달할 수 있다.
  • 함수 호출을 인라인(inline)화해서 효율성을 향상시킬 수 있다.
  • 함수자의 정보를 캡슐화하여 지역화(locally)할 수 있다.

 

 2, STL 함수자

STL 함수자는 크게 두 가지로 나눌 수 있습니다.

  • 1, 일반 함수자 : 특정 기능을 수행하는 함수자.

    • 산술 연산 함수자 : 산술 연산 기능을 수행.
    • 비교 연산 함수자 : 비교 연산 기능을 수행.
    • 논리 연산 함수자 : 논리 연산 기능을 수행.

       

  • 2, 함수 어댑터 : 함수류(함수와, 함수 포인터, 함수자)를 인자로 받아 새로운 함수자를 생성.

    • 바인더(binder) : 이항 함수자를 단항 함수자로 변환하는 함수자.
    • 부정자(negator) : 조건자(predicate)를 반대로 변환하는 함수자.
    • 함수 포인터 어댑터(adaptors for pointersto functions) : 함수 포인터를 라이브러리가 필요로 하는 함수자로 변환하는 함수자
    • 멤버 함수 포인터 어댑터(adaptors for pointers to member functions) : 멤버 함수 포인터를 라이브러리가 필요로 하는 함수자로 변환하는 함수자

어댑터의 인자로 사용되는 함수자는 아래 요구 사항을 지켜야 합니다.

  • 단항 함수자는 반드시 argument_type, result_type이 정의되어 있어야 한다. 각각은 함수의 인자 타입과 리턴 타입이다.
  • 이항 함수자는 반드시 first_argument_type, second_argument_type, result_type이 정의되어 있어야 한다. 각각은 함수의 첫번째 인자 타입과 두 번째 인자 타입, 리턴 타입이다.

어댑터들은 인자로 받는 함수자의 위 정의 타입을 이용하여 컴파일 시간에 타입들을 검사합니다.

이러한 타입 정의를 쉽게하기 위해 STL은 기본 클래스 unary_function과 binary_function을 제공한다. 그래서 위 타입들을 직접 작성할 필요는 없습니다. 어댑터는 무조건 위 기본 클래스를 상속받아 만들면 됩니다. ㅡㅡ;

 

또 함수류(함수, 함수 포인터, 함수자) 중에서 bool형을 리턴하는(비교에 사용되는) 함수자를 predicate(조건자, 술어 구문)라 합니다.

 

2.1, 일반 함수자

일반 함수자에는 산술 연산, 비교 연산, 논리 연산 등을 수행하는 함수자들이 있습니다.

 

2.1.1, 산술 연산 함수자

 산술 연산자에는 plus, minus, multiplies, divides, modulus, negate가 있습니다. 모두 템플릿입니다.

 

plus의 간단한 예제입니다.

#include <iostream>
#include <functional>
using namespace std;

void main( )
{
    int result;

    plus<int> oPlus;
    result = oPlus( 10, 20); //oPlus 객체 생성 후 호출
    cout << result << endl;

    result = plus<int>()(10, 20); //임시 객체 생성 후 호출
    cout << result << endl;

}
  1. 30
    30

두 인자를 받아 합을 리턴하는 함수자입니다.

 

그렇다면 이런 함수자를 어디에 써먹나? ㅡㅡ;;

STL은 함수자없이는 동작할 수 없을 정도로 많은 곳에 함수자가 사용됩니다. 

plus 함수자를 사용하는 실제 예제를 보도록 하겠습니다.

 

plus 함수자를 사용한 trasform 알고리즘입니다.

 #include <iostream>
#include <functional>
#include <algorithm>
#include <vector>
using namespace std;

void main( )
{
    vector<int> v1;
    vector<int> v2;


    v1.push_back(10); 

    v1.push_back(20);
    v1.push_back(30);

    v2.push_back(1);
    v2.push_back(2);
    v2.push_back(3);

    for(int i = 0 ; i < v1.size() ; i++)
        cout << v1[i] << " ";
    cout << endl;

    for(int i = 0 ; i < v2.size() ; i++)
        cout << v2[i] << " ";
    cout << endl;

    transform(v1.begin(), v1.end(), v2.begin(), v1.begin(), plus<int>());

    for(int i = 0 ; i < v1.size() ; i++)
        cout << v1[i] << " ";
    cout << endl;
}

  1. 10 20 30
    1 2 3
    11 22 33

 v1.begin()~v1.end()까지의 원소와 v2의 원소를 함수자(plus<int>())로 연산하여 리턴 결과를 다시 v1.begin()부터 순서대로 저장하는 예제입니다.

그림으로 보면 아래와 같습니다.

plus.png 

 

 

STL plus를 사용하지 않고 사용자 Plus 함수자를 만들어 보도록 하겠습니다.

직접 작성한 Plus 함수자 예제입니다.

#include <iostream>
#include <functional>
#include <algorithm>
#include <vector>
using namespace std;


struct Plus
{
    int operator( )(int a, int b) const
    {
        return a+b;
    }
};
void main( )
{
    vector<int> v1;
    vector<int> v2;


    v1.push_back(10);

    v1.push_back(20);
    v1.push_back(30);

    v2.push_back(1);
    v2.push_back(2);
    v2.push_back(3);

    for(int i = 0 ; i < v1.size() ; i++)
        cout << v1[i] << " ";
    cout << endl;

    for(int i = 0 ; i < v2.size() ; i++)
        cout << v2[i] << " ";
    cout << endl;

    transform(v1.begin(), v1.end(), v2.begin(), v1.begin(), Plus() );

    for(int i = 0 ; i < v1.size() ; i++)
        cout << v1[i] << " ";
    cout << endl;
}

  1. 10 20 30
    1 2 3
    11 22 33

 결과는 같습니다. 직접 작성한 Plus 함수자를 사용했습니다.

 

STL의 plus처럼 어떤 타입의 합도 계산할 수 있도록 일반화 시키려면 템플릿 클래스로 만들면 됩니다.

다음은 Plus를 템플릿 클래스로 바꾼 예제입니다.

#include <iostream>
#include <functional>
#include <algorithm>
#include <vector>
using namespace std;

template<typename T>
struct Plus
{
    T operator( )(const T& a, const T& b) const
    {
        return a+b;
    }
};
void main( )
{
    vector<int> v1;
    vector<int> v2;


    v1.push_back(10);

    v1.push_back(20);
    v1.push_back(30);

    v2.push_back(1);
    v2.push_back(2);
    v2.push_back(3);

    for(int i = 0 ; i < v1.size() ; i++)
        cout << v1[i] << " ";
    cout << endl;

    for(int i = 0 ; i < v2.size() ; i++)
        cout << v2[i] << " ";
    cout << endl;

    transform(v1.begin(), v1.end(), v2.begin(), v1.begin(), Plus<int>() );

    for(int i = 0 ; i < v1.size() ; i++)
        cout << v1[i] << " ";
    cout << endl;
}

  1. 10 20 30
    1 2 3
    11 22 33

단순히 템플릿 클래스로 변환한 것 외에 다른 것은 없습니다.

 

마지막으로 2, STL 함수자에서 언급했던 것처럼 만약 위에서 작성한 Plus<> 함수자 클래스를 어댑터 적용이 가능하도록 하려면 이항 함수자이므로 first_argument_type, second_argument_type, result_type을 정의해야 합니다. 가장 간단하게 정의할 수 있는 방법은 binary_function 기본 클래스를 상속받는 것입니다. 위 정의 타입이 필요한 이유는 어댑터에서 컴파일 시간에 타입 검사를 할때 위 정의 타입이 필요하기 때문입니다.

어댑터에 자세한 내용은 아래쪽에서 공부합니다.

 

다음은 binary_function을 상속받은 Plus<> 함수자 클래스입니다.

#include <iostream>
#include <functional>
#include <algorithm>
#include <vector>
using namespace std;

template<typename T>
struct Plus : public binary_function<T, T, T>
{
    T operator( )(const T& a, const T& b) const
    {
        return a+b;
    }
};
void main( )
{
    vector<int> v1;
    vector<int> v2;

    v1.push_back(10);
    v1.push_back(20);
    v1.push_back(30);

    v2.push_back(1);
    v2.push_back(2);
    v2.push_back(3);

    for(int i = 0 ; i < v1.size() ; i++)
        cout << v1[i] << " ";
    cout << endl;

    for(int i = 0 ; i < v2.size() ; i++)
        cout << v2[i] << " ";
    cout << endl;

    transform(v1.begin(), v1.end(), v2.begin(), v1.begin(), Plus<int>() );

    for(int i = 0 ; i < v1.size() ; i++)
        cout << v1[i] << " ";
    cout << endl;
}
  1. 10 20 30
    1 2 3
    11 22 33

지금 예제에는 어댑터를 사용하지 않기 때문에 굳이 필요하지는 않지만 다음을 위해서 어댑터 적용이 가능하도록 미리 binary_function을 상속받았습니다.

 

간단한 예제를 보이도록 하겠습니다.(binder1st<> 어댑터 사용)

#include <iostream>
#include <functional>
#include <algorithm>
#include <vector>
using namespace std;

void main( )
{
    vector<int> v;

    v.push_back(10);
    v.push_back(20);
    v.push_back(30);

    for(int i = 0 ; i < v.size() ; i++)
        cout << v[i] << " ";
    cout << endl;

    transform(v.begin(), v.end(), v.begin(), binder1st< plus<int> > (plus<int>(), 100) );

    for(int i = 0 ; i < v.size() ; i++)
        cout << v[i] << " ";
    cout << endl;
}
  1. 10 20 30
    110 120 130

v.begin()~v.end()까지의 모든 원소를 네 번째 함수자의 인자로 전달하고 리턴한 결과를 다시 v.begin()부터 순서대로 저장합니다.

 binder1st<>는 이항 연산자를 단항 연산자로 바꾸는 바인더 어댑터입니다. 위 예제의 transform이 단항 연산자를 필요로 하기때문에 이항 연산이 가능한 plus 함수자를 오른쪽 항을 100으로 고정시키고 왼쪽항만 인자로 받을 수 있는 단항 연산자 함수자로 변환합니다.

어댑터는 아래쪽에서 다시 자세히 공부합니다. 지금은 위와 같은 어댑터에 사용하는 plus 함수자는 binary_function 기본 클래스를 상속받아야 한다는 것에 집중하시면 됩니다.

 

우리가 만든 Plus<>가 binary_function을 상속받지 않으면 무시무시한(?) 에러가 발생합니다.

#include <iostream>
#include <functional>
#include <algorithm>
#include <vector>
using namespace std;

template<typename T>
struct Plus
{
    T operator( )(const T& a, const T& b) const
    {
        return a+b;
    }
};
void main( )
{
    vector<int> v;

    v.push_back(10);
    v.push_back(20);
    v.push_back(30);

    for(int i = 0 ; i < v.size() ; i++)
        cout << v[i] << " ";
    cout << endl;

    transform(v.begin(), v.end(), v.begin(), binder1st< Plus<int> > (Plus<int>(), 100) );

    for(int i = 0 ; i < v.size() ; i++)
        cout << v[i] << " ";
    cout << endl;
}
  1. error C2039: 'second_argument_type' : 'Plus<T>'의 멤버가 아닙니다.
    1>        with
    1>        [
    1>            T=int
    1>        ]
    1>        c:\...\main.cpp(28) : 컴파일 중인 클래스 템플릿 인스턴스화 'std::binder1st<_Fn2>'에 대한 참조를 확인하십시오.
    1>        with
    1>        [
    1>            _Fn2=Plus<int>
    1>        ]
  2. ....

무서버~!

 

binary_function 기본 클래스를 상속받으면 쉽게 해결됩니다.

#include <iostream>
#include <functional>
#include <algorithm>
#include <vector>
using namespace std;

template<typename T>
struct Plus : public binary_function<T, T, T>
{
    T operator( )(const T& a, const T& b) const
    {
        return a+b;
    }
};
void main( )
{
    vector<int> v;

    v.push_back(10);
    v.push_back(20);
    v.push_back(30);

    for(int i = 0 ; i < v.size() ; i++)
        cout << v[i] << " ";
    cout << endl;

    transform(v.begin(), v.end(), v.begin(), binder1st< Plus<int> > (Plus<int>(), 100) );

    for(int i = 0 ; i < v.size() ; i++)
        cout << v[i] << " ";
    cout << endl;
}
  1. 10 20 30
    110 120 130

 간단합니다.

 

우리가 직접 first_argument_type, second_argument_type, result_type을 정의해도 해결됩니다.

#include <iostream>
#include <functional>
#include <algorithm>
#include <vector>
using namespace std;

template<typename T>
struct Plus
{
    typedef T first_argument_type;
    typedef T second_argument_type;

    typedef T result_type;

    T operator( )(const T& a, const T& b) const
    {
        return a+b;
    }
};
void main( )
{
    vector<int> v;

    v.push_back(10);
    v.push_back(20);
    v.push_back(30);

    for(int i = 0 ; i < v.size() ; i++)
        cout << v[i] << " ";
    cout << endl;

    transform(v.begin(), v.end(), v.begin(), binder1st< Plus<int> > (Plus<int>(), 100) );

    for(int i = 0 ; i < v.size() ; i++)
        cout << v[i] << " ";
    cout << endl;
}

  1. 10 20 30
    110 120 130

상속이라는 더 깔끔한 방법이 있으니 binary_function을 상속받는 것이 더 좋겠죠?

 

minus, multiplies, divides, modulus, negate함수자는 plus와 모두 비슷하므로 생략합니다.

 

2.1.2, 비교 연산 함수자

 비교 연산자는 equal_to, not_equal_to, greater, less, greater_equal, less_equal이 있습니다. 모두 템플릿 클래스입니다.

 비교 연산 함수자는 모두 predicate입니다. predicate(조건자)는 비교를 수행하는(bool형을 리턴하는 ) 함수류(함수, 함수 포인터, 함수자)을 말합니다.

 

greater의 간단한 예제입니다.

#include <iostream>
#include <functional>
using namespace std;

void main( )
{
    int result;

    greater<int> oGreater;
    result = oGreater( 10, 20); //객체 생성 후 호출
    cout << result << endl;

    result = greater<int>()(10, 20); //임시 객체 생성 후 호출
    cout << result << endl;

    if( greater<int>()(50, 20) )
        cout << "true" << endl;
}
  1. 0
    0
    true

 greater는 두 인자를 받아 첫 번째 인자가 크면 true를 아니면 false를 리턴합니다.

 

 실제 사용 예제입니다.(sort 알고리즘)

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
using namespace std;

void main( )
{
    vector<int> v;

    v.push_back(50);
    v.push_back(20);
    v.push_back(40);
    v.push_back(30);
    v.push_back(10);

    for(int i = 0 ; i < v.size() ; i++)
        cout << v[i] << " ";
    cout << endl;


    sort(v.begin(), v.end()); 
    for(int i = 0 ; i < v.size() ; i++)
        cout << v[i] << " ";
    cout << endl;

    sort(v.begin(), v.end(), greater<int>() );

    for(int i = 0 ; i < v.size() ; i++)
        cout << v[i] << " ";
    cout << endl;
}

  1. 50 20 40 30 10
    10 20 30 40 50
    50 40 30 20 10

 sort는 less를 사용하여 오름차순 정렬을 수행하지만 세 번째 인자로 greater를 전달하면 내림차순 정렬을 합니다.

 

 다음은 직접 작성한 Greater<>입니다.( 어댑터 적용이 가능하도록 binary_function을 상속 받았습니다.)

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
using namespace std;

template<typename T>
struct Greater : public binary_function<T, T, bool>
{
    bool operator()(const T& a, const T& b) const
    {
        return a > b;
    }
};
void main( )
{
    vector<int> v;

    v.push_back(50);
    v.push_back(20);
    v.push_back(40);
    v.push_back(30);
    v.push_back(10);

    for(int i = 0 ; i < v.size() ; i++)
        cout << v[i] << " ";
    cout << endl;


    sort(v.begin(), v.end()); 
    for(int i = 0 ; i < v.size() ; i++)
        cout << v[i] << " ";
    cout << endl;

    sort(v.begin(), v.end(), Greater<int>() );

    for(int i = 0 ; i < v.size() ; i++)
        cout << v[i] << " ";
    cout << endl;
}

  1. 50 20 40 30 10
    10 20 30 40 50
    50 40 30 20 10

결과는 같습니다.

 

나머지 비교 연산 equal_to, not_equal_to, less, greater_equal, less_equal은 greater와 비슷하므로 생략합니다.

 

2.1.3, 논리 연산 함수자

 논리 연산은  logical_and, logical_or, logical_not가 있습니다.

 

logical_and의 간단한 예제입니다.(논리 AND 연산을 수행하는 함수자)

#include <iostream>
#include <functional>
using namespace std;

void main( )
{
    int result;

    logical_and<int> and;
    result = and( 10 == 10, 1 < 2); //객체 생성 후 호출
    cout << result << endl;

    result = logical_and<int>()(10 > 20, 'A' < 'B'); //임시 객체 생성 후 호출
    cout << result << endl;

    if( logical_and<int>()(1, 1) )
        cout << "true" << endl;
}
  1. 1
    0
    true

 두 인자를 입력받아 두 인자 모두 true이면 true를 리턴하고 아니면 false를 리턴합니다. AND 연산!

 

간단한 사용 예제입니다.

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
using namespace std;

void main( )
{
    vector<bool> v1;
    vector<bool> v2;
    vector<bool> v3(3);


    v1.push_back(true);

    v1.push_back(false);

    v1.push_back(true);


    v2.push_back(false);
    v2.push_back(false);
    v2.push_back(true);

    transform(v1.begin(), v1.end(), v2.begin(), v3.begin(), logical_and<bool>() );

    for(int i = 0 ; i < v3.size() ; i++)
    {
        if( v3[i] )
            cout << "true  ";
        else
            cout << "false  ";
    }
    cout << endl;
}

  1. false  false  true

 간단하죠?

 

너머지는 모두 비슷하므로 생략합니다.

 

 오늘은 여기까지입니다. ^^

...

 

반응형
반응형

#include <iostream>
#include <list>
#include <functional>
#include <algorithm>

using namespace std;


class fuctor{

public :
 int m_MemData;
 static int m_data;

public :
 fuctor(int ini) {
  m_MemData = ini;
 }
 fuctor() {
  m_MemData = m_data++;
 }
 int operator+( int data ) const{
  return this->m_MemData + data;
 }
 void show(int data){
  cout<<m_MemData + data<<"\t";
 }


};

int fuctor::m_data = 1;


class IntSequence{

public :
 fuctor operator()(){
  return fuctor();
 }

};

template<typename T1,typename T2>
class plus1 : public binary_function<T1,T2,T1>{
public :
 T1 operator()(const T1 lhs,const T2 rhs) const {
  return  T1(lhs + rhs);
 }
};

 

int main(){

 list<fuctor> coll2;
 
 generate_n( back_inserter(coll2),10,  IntSequence() );

 transform(coll2.begin(),coll2.end(),coll2.begin(), bind2nd( plus1<fuctor,int>(),10 ) );

 for_each( coll2.begin(),coll2.end(), bind2nd(mem_fun_ref(&fuctor::show) ,100)  );
 
 if(getchar())
  return 0;
}

반응형
반응형

emplate<typename OP1,typename OP2>
class compose_f_gx_t : public unary_function< typename OP2::argument_type,typename OP1::result_type>
{

private :
 OP1 op1; // op(op2(x)) 를 처리한다
 OP2 op2;
public :
 compose_f_gx_t(const OP1& o1, const OP2& o2) : op1(o1), op2(o2)
 {

 }
 typename OP2::result_type
  operator()(const typename OP2::argument_type& x) const {
   return op1(op2(x));
 }

};

template<typename OP1,typename OP2>
inline compose_f_gx_t<OP1,OP2> compose_f_gx(const OP1& o1, const OP2& o2){
 return compose_f_gx_t<OP1,OP2>(o1,o2);
}

반응형
반응형

STL List 에서 기본 자료형이 아닌 클래스 또는 구조체 정렬 하기.

class CObject
{
public:
        int        GetCurX(void);
        int        GetCurY(void) const;
};

list g_stlObjectList;

이런 식으로 직접 만든 클래스를 STL List에 사용하고
클래스 맴버변수에 따른 정렬을 하고 싶다면 operator() 를 사용해서
비교 함수를 재정의 해주시면 됩니다.

struct stLISTsort
{
        bool operator() (const CObject *pObject1, const CObject *pObject2) const
        {
                if ( pObject1->GetCurY() < pObject2->GetCurY() )
                        return true;

                return false;
        }
};

크다 작다 TRUE/FALSE 로 넣어주시면 됩니다.


g_stlObjectList.sort(stLISTsort());

그리고 이와 같이 sort 호출시 재정의된 구조체 또는 클래스를 넣어서
호출하면 위의 연산자로써 정렬을 하게 됩니다.


이 방법 외에도 여러가지 방법이 있습니다.


반응형
반응형

#include <memory>
#include <vector>
#include <list>
#include <Windows.h>
#include <string.h>
#include<iostream>
#include <algorithm>

using namespace std;

 


class node{
private :

 int m_data;

public :
 int getData(){ return m_data; }
 
 node(){
  int templ=30;
  int ddd;
 }
 node(int data){

  m_data=data;
 }
 ~node(){

 }
 bool operator()(node* nodeData){
  if( nodeData->getData() == 30 )
   return true;
  else
   return false;
 }
 void print(){
  cout<<m_data<<"\t";
 }
};

int main(){


 vector<node*> nodes;

 node* pNode=NULL;
 for(int i=0;i<10;++i){
  nodes.push_back(new  node(i*10));
 }

 // mem_fun은 클래스의 멤버 함수를 함수 객체로 만든다.  : 함수객체는 타입이 될 수 있어 조건자로 들어 갈 수 있다
 // mem_fun_ref 는 객체를 넘길때 & 로 넘기는 방식이다
 for_each( nodes.begin(),nodes.end(), mem_fun( &node::print ) );  //http://www.winapi.co.kr/clec/cpp4/38-2-5.htm

 cout<<(*find_if( nodes.begin(),nodes.end(), node() ))->getData()<<endl;

 vector<node*>::iterator it=nodes.begin();
 for(;it!=nodes.end();++it){
  delete (*it);
 }
 nodes.clear();

 return 0;
}

반응형
반응형

C++의 STL 표준 라이브러리는 미리 정의된 여러가지 함수 객체들을 제공한다.

이러한 표준 함수객체들을 사용하기 위해서는 #include <functional> 을 포함해야 한다.

 

이 함수객체들은 알고리즘의 비교,정렬을 위한 기준으로 사용될 수 있다.

less<> 는 알고리즘에서 디폴트 기준으로 사용되는 오름차순 정렬이다.

 

negate<type>()

- 파라미터

plus<type>()

파라미터1 + 파라미터2

minus<type>()

파라미터1 - 파라미터2

multiplies<type>()

파라미터1 * 파라미터2

divides<type>()

파라미터1 / 파라미터2

modulus<type>()

파라미터1 % 파라미터2

equal_to<type>()

파라미터1 == 파라미터2

not_equal_to<type>()

파라미터1 != 파라미터2

less<type>()

파라미터1 < 파라미터2

greater<type>()

파라미터1 > 파라미터2

less_equal<type>()

파라미터1 <= 파라미터2

great_equal<type>()

파라미터1 >= 파라미터2

logical_not<type>()

!파라미터

logical_and<type>()

파라미터1 && 파라미터2

logical_or<type>()

파라미터1 || 파라미터2

 

 

1. 함수어댑터

 

함수어댑터란, 함수객체를 특정값 또는 특정함수와 결합시킨 함수객체이다.

이러한 함수어댑터 역시 <functional> 에 정의되어 있다.

 

표현식

효과

bind1st(op, value)

op(value, 파라미터)

bind2nd(op, value)

op(파라미터, value)

not1(op)

!op(파라미터)

not2(op)

!op(파라미터1, 파라미터2)

 

find_if(coll.begin(), coll.end(), bind2nd(greater<int>(), 42));

이 코드에서 bind2nd(greater<int>(), 42) 는 정수타입 변수가 42보다 큰지를 판단하는 결합된 함수객체를 생성한다.

 

bind2nd는 greater<> 와 같은 이항 함수객체를 단항 함수객체로 변형시킨다.

즉, bind2nd의 두번째 인자를 이항 함수객체의 두번째 인자로 사용하는 것이다.

따라서, greater<>는 42를 두번째 인자로 하여 호출한다.

 

함수어댑터는 스스로가 함수객체이다. 따라서, 함수어댑터를 함수객체와 결합하여 사용할 수 있다.

pos=find_if(coll.begin(), coll.end(), not1(bind2nd(modulus<int>(), 2)));

 

이 코드에서 bind2nd(modulus<int>(), 2) 는 모든 홀수에 대해 1(참) 을 반환한다.

따라서, 이 표현식은 홀수값을 갖는 첫번째 원소를찾는 기준이된다.

하지만 not1() 은 이 결과를 반전시키므로, 짝수값을 갖는 첫번째 원소의 위치를 반환한다.

 

 

2. 멤버함수를 위한 함수어댑터

 

이것은 STL 컨테이너의 각각의 원소들에 대해서 멤버함수를 호출시켜주는 함수 어댑터이다.

표현식효과
mem_fun_ref(op)객체에 대해서 멤버함수 op() 를 호출
mem_fun(op)객체의 포인터에 대해서 멤버함수 op()를 호출

 

#include <iostream>
#include <vector>
#include <functional>
using namespace std;


class person{
 private:
  string name;
 public:
  person(char *nm):name(nm){}
  void print()const{
   cout<<name<<endl;
  }
  void printWithPrefix(string prefix)const{
   cout<<prefix<<name<<endl;
  }
};

int main(){
 vector<person> coll;
 coll.push_back(person("AAAA"));
 coll.push_back(person("BBBB"));
 coll.push_back(person("CCCC"));
 coll.push_back(person("DDDD"));

 

 //각각의 원소에 대해서 멤버함수 print()를 호출
 for_each(coll.begin(), coll.end(), mem_fun_ref(&person::print));
 
 //각각의 원소에 대해서 멤버함수 printWithPrefix()를 호출
 //"person : " 은 멤버함수 printWithPrefix() 의 인자로 전달
 for_each(coll.begin(), coll.end(), bind2nd(mem_fun_ref(&person::printWithPrefix), "person : "));
}

 

3. 일반 함수들을 위한 함수어댑터

 

표현식

효과

ptr_fun(op)

*op(파라미터)

*op(파라미터1, 파라미터2)

 

ptr_fun() 은 기존의 일반 함수들을 함수객체와 같은 형태로 사용하도록 하는 함수객체 어댑터이다.

 

예를들어, 각각의 인자에 대해 무언가를 검사하는 일반 전역함수를 가지고 있을때

bool check(int elem);

 

이러한 경우 조건을 만족하는 첫번째 원소를 찾으려면 다음과 같이 호출한다.

pos=find_if(coll.begin(), coll.end(), ptr_fun(check));

 

조건을 만족하지않는 첫번째 원소를 찾으려면 다음과 같이 사용할 수 있다.

pos=find_if(coll.begin(), coll.end(), not1(ptr_fun(check)));

 

사용자가 두개의 인자를 사용하는 전역함수를 가지고 있다고 가정한다면

pos=find_if(coll.begin(), coll.end(), not1(bind2nd(ptr_fun(strcmp), "AAA")));

 

strcmp는 두개의 string이 같을경우 false를 반환한다.

따라서, 이 구문은 "AAA" 와 같은 첫번째 string의 위치를 반환한다.

 

반응형
반응형


복사http://blog.naver.com/jdpb13/80166370289

커널 모드로 전환을 하지 않기 때문에 성능상 이점을 가진다.

크리티컬 섹션(Critical Section) 기반 동기화

화장실 열쇠라고 생각하면 된다. 임계 영역을 화장실이라고 하면 이 화장실에 들어가기 위해서는 열쇠가 필요하다. 즉, 한 사람이 쓰고 나와야 다른 사람이 사용할 수 있다는 것이다.

크리티컬 섹션 기반 동기화를 사용하기 위해 크리티컬 섹션 오브젝트를 만들고 초기화 해야 한다. 크리티컬 섹션 오브젝트는 CRITICAL_SECTION 자료형을 의미한다.

1.

CRITICAL_SECTION key; // 화장실 키 만듦

2.

크리티컬 섹션 오브젝트 초기화.

void WINAPI InitializeCriticalSection(
__out LPCRITICAL_SECTION lpCriticalSection
); // 키를 걸어둠.

3.

void WINAPI EnterCriticalSection(
__inout LPCRITICAL_SECTION lpCriticalSection
); // 열쇠 획득 후 화장실 사용

만약 누군가가 이미 이 함수를 호출할 경우 호출된 함수는 블로킹 상태가 된다. 그리고 열쇠가 반환될 떄 까지 기다린다.

4.

void WINAPI LeaveCriticalSection(
__inout LPCRITICAL_SECTION lpCriticalSection
); // 화장실 사용 후 열쇠를 제자리에

EnterCriticalSection함수를 호출하여 블로킹 상태에 놓인 쓰레드는 사용중인 쓰레드가 이 함수를 호출해야 화장실에 들어갈 수 있다.

화장실 키 생성 -> 키를 걸어두고 -> 키를 가지고 화장실 사용 -> 다시 키를 제자리로

위 순서라고 생각하면 된다.

한 순간에 하나의 쓰레드만 실행할 수 있도록 구성하는 것이 크리티컬 섹션 동기화 기법의 핵심이다.

DeleteCriticalsection을 이용하여 할당된 리소스들을 반환해야 한다.

  

인터락 함수(Interlocked Family Of Function) 기반의 동기화

변수 하나의 접근방식만 동기화 하려면 이 동기화 기법을 사용하는 것이 좋을 것이다.

LONG __cdecl InterlockedIncrement(
__inout LONG volatile *Addend
);

값을 하나 증가시킬 변수의 주소 값을 전달한다.

LONG __cdecl InterlockedDecrement(
__inout LONG volatile *Addend
);
값을 하나 감소시킬 변수의 주소 값을 전달한다.

int jdpb=10;

InterlockedIncrement(&jdpb);

두 함수를 통해 증감할 경우 동기화 된 상태에서 접근하는 것과 동일하게 사용이 가능하다. 즉, 하나의 쓰레드만 접근해주도록 해주는 함수이다.

  

http://msdn.microsoft.com/en-us/library/windows/desktop/ms683614(v=vs.85).aspx 에서 함수 참조 및 검색.

반응형
반응형

BLOG main image





[ 주의사항 ]


criticalsection 사용시 시스템 메모리가 부족하 경우 제대로 동기화되지 않고 오류가 발생한다


이상황을 방지하기위해서 try-catch 를 사용하거나 InitializeCriticalSectionAndSpinCount 함수를 사용해야한다


자세한사항은 :  http://3dmpengines.tistory.com/1168







출저:  http://blog.naver.com/jeon_sw?Redirect=Log&logNo=110013411452 

 

윈도우가 쓰레드의 동기화에 Mutex 와 더불어 CRITICAL_SECTION 이라는 객체를 사용하고 있기 때문에 의미상의 오해가 있을 수 있습니다만, 일반적으로 OS 에서 Crtical Section 이라고 하는 것은 서로 다른 쓰레드나 프로세스가 간섭하지 않고 순차적으로 실행되어야 하는 영역을 뜻하고, 보통 임계 영역 이라고 해석합니다.

제목에 포함된 Critical Section 은 이 영역의 제어를 어떻게 할까를 생각해 보자는 뜻으로 붙인겁니다.

 

Critical Section 의 제어를 어떻게 할지 생각하자?

대부분이 Mutex 나 Semaphore, CRITICAL_SECTION 을 쓰면 되지 않느냐라고 답할겁니다.

 

당연히 그러면 됩니다. 그걸 몰라서가 아니라, 좀 더 성능을 향상하기 위해서는 약간 더 고려해야 될 부분이 있을 거 같아서 입니다.

고려 대상은 주로 여러개의 CPU 를 사용하는 서버의 경우입니다. Uni-Processor 인 일반 PC 에서 실행되는 어플리케이션의 개발은 그냥 기존의 방법을 사용해도 되니까, 이런 어플리케이션을 주로 사용하시는 분은 지금 바로 과감히 다른 페이지나 게시물로 옮겨 가시기 바랍니다.

 

그러면, Multi-Processor 상황에서 어떤 경우를 고려해야 하느냐 하면, Critical Section 이 아주 짧은 경우입니다.

OS에 관한 지식이 약간 있으신 분은 spin-lock 과 Busy-waiting 에 대해 말하고 싶은 거다라는 걸 금방 아셨을 겁니다.

 

spin-lock이 뭔지를 모르시는 분들을 위해 잠시 부가 설명을 하겠습니다.

 

예를 들어 globalCount 라는 공유메모리가(쓰레드간 또는 프로세스간에 공유되는 값) 있다고 가정할 때,

이 값이 순차적으로 증가, 감소하는 값이라고 하면 이 값의 증가에 일반적으로 다음과 같은 방법을 사용합니다.

 

EnterCriticalSection(&cs);

globalCount++;

LeaveCriticalSection(&cs);

 

프로세스간 공유라고 하면,

 

if (WaitForSingleObject(hMutex, millisec) == WAIT_OBJECT_0)

{

    globalCount++;

    ReleaseMutex(hMutex);

}

 

주로 이런 방법을 사용할겁니다. 여기까지 생각이 드셨다면 일반적인 프로그래머라고 생각되고,

 

약간 더 고급 프로그래머라면,

 

InterlockedIncrement(&globalCount);

 

를 생각할 수도 있을겁니다.

 

가장 좋은 방법은 InterlockedIncrement 이지만, 이 함수는 논외로 하겠습니다.

내부적으로 Intel 의 lock inc Instruction-set 을 호출하므로, 이 함수는 정확히 32bit 값에 대해서만 Atomic 처리가 가능하기 때문입니다.(물론 64bit CPU는 64bit입니다만)

지금 예로 든건 편의상 globalCount++ 일 뿐이지, 실제로는 globalCount++; globalCount2--; 일 수도 또는 약간 더 길 수도 있을 겁니다.

 

그럼, 앞의 두가지 경우를 다시 생각해 보겠습니다.

Mutex 는 CRITICAL_SECTION 에 비해 상대적으로 비용이 비싼 객체이므로 우선 대상은 CRITICAL_SECTION 으로 정하겠습니다.

임계영역에서 실행되는 globalCount++; 는 극히 짧은 연산입니다. 이 연산을 위해서 EnterCriticalSection 을 두 쓰레드가 동시에

호출하게 되면, 하나의 쓰레드는 Sleep 상태로 빠지게 될겁니다. 그리고 Sleep 이 되지 않은 쓰레드가 LeaveCriticalSection 을

호출한 후다시 깨어나서 EnterCriticalSection 을 재시도 하여 성공하면 globalCount++ 를 실행하게 됩니다.

이런 상황은 임계영역이 짧은 경우, 하나의 CPU 를 사용하는 PC에서는 거의 발생하지 않습니다. 왜냐하면, 하나의 CPU를 사용하는

시스템에서 동시에 실행되는 작업이란 없기 때문입니다. 임계영역 내에서 Context-Switch 가 발생하지 않는다면, 절대로 일어날

수 없는 상황입니다.

 

그러나, 여러개의 CPU로 동작하는 서버의 경우 실제적으로 여러 쓰레드가 동시에 실행되는 상황이 발생합니다. 그러므로, 아무리

임계영역이 짧아도 이 루틴의 호출빈도가 잦다면, 이런 상황이 자주 발생할 수 있습니다.

그러면, 한 쓰레드가 Blocking 됐다가 깨어나는 작업이 왜 문제가 되느냐?

이 작업이 결코 공짜가 아니라는 데에 문제가 있습니다. 잦은 Context-Switching 은 시스템의 성능을 저하시킵니다.

 

이런 문제를 해결하는 하나의 방법으로 spin-lock 이라는 걸 사용하는 겁니다. 어떻게 동작하느냐 하면,

 

int nSpins;

for (nSpins = spinCount; nSpins > 0; nSpins--)

    if (TryEnterCriticalSection(&cs))

        break;

if (nSpins == 0)

{

    // 처리불가, Sleep 또는 EnterCriticalSection(&cs)); 호출

}

 

와 같은 형태로 구성됩니다. spinCount 횟수만큼 임계영역으로 들어갈 수 있는지를 시도해 보는 겁니다.

들어갈 수 있으면 임계영역의 코드를 실행하고, 들어갈 수 없으면 다시 재시도 합니다.

spinCount는 CPU 갯수 - 1 로 최적화 시키는 것이 일반적일 겁니다.(당연히 CPU가 하나뿐인 시스템에는 적용 안합니다)

이유는 현재 쓰레드가 실행되는 CPU 를 제외한 다른 CPU 가 임계영역을 빠져 나오기를 기다리는 것이 가장 합리적이기 때문입니다.

 

그러므로, 임계영역이 짧은 경우는 일반적인 EnterCriticalSection 보다 이런 형태의 spin-lock 을 사용하는 것이 더 효과적입니다.

CRITICAL_SECTION은 이 처리를 위해 예로 제시한 루프를 돌리지 않아도 윈도우가 자동으로 이 작업을 해 주는 경우가 있습니다.

InitializeCriticalSectionAndSpinCount 로 CRITICAL_SECTION을 초기화할 경우입니다.

그러나, 주의할 점은 임계영역이 긴 경우, 또는 임계영역 내에서 I/O 함수를 호출할 경우, 또는 윈도우 API 를 호출하는 경우 등등의 경우는 이런 방법을 사용하지 않는게 좋습니다.

아니, 사용하지 않는게 좋은게 아니라 절대 사용하면 안됩니다.

 

앞에서도 언급했지만, 숫자값의 연산과 같은 아주 단순한 처리를 행하는 짧은 임계영역을 사용할 때를 대상으로 하는 겁니다.

 

쓰레드간의 공유에 대해서는 TryEnterCriticalSection 이나 InitializeCriticalSectionAndSpinCount 로 이런 문제를 해결할 수 있습니다.

그러면, 프로세스간의 공유에 대해서는 어떻게 될까요?

이 문제를 해결하기 위해서는 WaitForSingleObject(hMutex, 0) 을 대신 사용하는 방법밖에 없습니다.

WaitForSingleObject 는 상당히 덩치가 큰 API 입니다. spin-lock 을 처리하기 위해 이렇게 긴 API 를 호출한다는건 조금 비합리적이지 않을까요?

그래서 윈도우 프로그램에서는 일반적으로 프로세스간의 공유에 있어서는 spin-lock 을 잘 사용하지 않습니다.

 

여기서, POSIX 의 뮤텍스를 잠시 살펴 보면, 뮤텍스 객체가 윈도우와 달리 커널에 존재하지 않습니다. 쓰레드간의 뮤텍스이건 프로세스간의 뮤텍스이건, POSIX 의 뮤텍스는 윈도우의

CRITICAL_SECTION 과 비슷한 형태로 관리되기 때문에 윈도우의 뮤텍스에 비해 훨씬 가볍습니다. 그리고, 이런 형태의 spin-lock 의 처리도 용이합니다.

실제로 Unix 나 Linux 의 어플리케이션은 대부분 락을 걸기 전에 이런 시도를 거치는 경우가 많이 있는데, 윈도우 어플리케이션은 그런 경우를 거의 못본거 같습니다.

윈도우의 경우 클라이언트 어플리케이션이 더 많기 때문일 수도 있을거 같기는 합니다만...

 

윈도우에서도 단순히 spin-lock 을 통해 Busy-waiting 을 수행할 경우는 OS 의 지원을 받을 필요가 없기 때문에 커널 객체 없이 사용자모드 자체로 구현이 가능합니다.

그래서, 이런 기능을 수행하는 함수를 만들어 봤습니다.

 

void light_lock(long* _lock)

{

    int a;

    for (;;) {

        __asm {

            mov edx, _lock;

            cmp dword ptr[edx], 0;

            jne retry;

            mov eax, 1;

            xchg eax, dword ptr[edx];

            cmp eax, 0;

            jne retry;

        }

        break;

retry:

        __asm {

            pause;

        }

    }

}

 

void light_unlock(long* _lock)

{

    *_lock = 0;

}

 

쓰레드간의 동기화에 있어서는 long 타입의 변수 하나를 선언하고 초기값을 0 으로 설정합니다.

light_lock 을 수행해 성공한 쓰레드는 이 변수를 1 로 설정하고 임계영역으로 들어 갑니다.

그렇지 못한 쓰레드는 pause Instruction 을 호출하여 잠시 대기한 후 다시 시도합니다.

pause 는 펜티엄4 에서 새롭게 기능이 개선된 명령입니다. 이 명령을 수행함으로써 CPU 에게 spin-lock 을 수행한다는 힌트를 주게 됩니다.

 

프로세스간의 동기화에 있어서는 역시 long 타입의 변수를 공유메모리로 선언합니다. 윈도우는 공유메모리가 파일맵핑 뿐이므로 파일맵핑

메모리로 선언하여 위의 함수를 수행하면 됩니다.

 

Mutex 와는 비교가 안되고, CRITICAL_SECTION 보다도 더 가볍기 때문에 단순한 동기화가 필요한 경우 활용가치가 높을 거라 생각됩니다.

물론 Busy-waiting 이니까 절대 대기시간이 길지 않은 작업에만 사용해야 겠지만...

아직 실무 적용을 한 소스는 아니고, 일단 시스템 성능 향상을 위해 나름대로 생각해 본 방법입니다.

반응형
반응형



출처 :http://blog.naver.com/xtelite/50023358990

 

 

multithread programming(1)

 

MFC에서의 Multithread

 

OS는 구분하지 않지만 MFC는 사용자 편의를 위하여 두 가지 형태로 지원

 

1.     Worker thread

2.     User Interface thread

 

Worker thread

 

::AfxBeginThread() 함수를 이용

 

CWinThread* ::AfxBeginThread(

       AFX_THREADPROC pfnThreadProc,

       LPVOID pParam,

       int nPriority = THREAD_PRIORITY_NORMAL, // 기본적으로  Process 동일

       UINT nStackSize = 0,

       DWORD dwCreateFlags = 0,                      // 0 또는 CREATE_SUSPENDED

       LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL

);

 

우선 Thread를 이용할 함수를 먼저 정의한다

 

UINT ThreadFunc(LPVOID pParam)

{

       int option = (int)pParam;

       

}

 

만약 인자가 많은 경우에는

 

typedef struct tagPARAMS

{

       ...

} PARAMS;

 

와 같이 한 후에

 

PARAMS *pParams = new PARAMS;

CWinThread *pThread = ::AfxBeginThread(ThreadFunc, &pParams);

 

와 같이 하고

 

UINT ThreadFunc(LPVOID pParam)

{

       PARAMS *pThreadParams = (PARAMS *)pParam;

       ...

       delete pThreadParams;

 

       return 0;

}

 

와 같이 사용하면 된다.

 

Thread를 잠시 중지시키고 싶을 때는 주 Process에서

 

pThread->SuspendThread();

 

다시 돌리고 싶을 때는 주 Process에서

 

pThread->ResumeThread();

 

와 같이 하면 된다.(Thread 자신이 호출할 수는 없다.)

또는 경우에 따라서는

 

Sleep(2000);

 

과 같이 사용할 수도 있는데 이 경우는 제어권을 다른 Process에 넘겨 주게 된다.

 

Sleep(0);

 

와 같이 할 경우에는 우선 순위가 높거나 같은 Process에 넘겨 주고 우선 순위가 높거나

같은 Process가 없을 경우에는 아무 일도 생기지 않는다.

 

Thread를 종료시키고 싶을 때는 TerminateThread() 함수를 사용하면 되는데 이 경우 Thread 함수가

내부 정리를 하지 못할 수가 있기 때문에 다음과 같은 방법이 많이 사용된다.

 

static BOOL bContinue = TRUE;

CWinThread *pThread = ::AfxBeginThread(ThreadFunc, &bContinue);

 

UINT ThreadPrintNum(LPVOID pParam)

{

       BOOL *pbContinue = (BOOL *)pParam;

       while ( *pbContinue )

       {

             

       }

       return 0;

}

 

와 같이 하고 bContinue 값을 FALSE로 하면 Thread 함수가 종료된다.

 

Thread가 완전히 종료된 것을 확신해야 하는 경우에는

 

if ( ::WaitForSingleObject(pThread->m_hThread, INFINITE) )

{

       // 이곳은쓰레드가확실히종료된상태임

}

 

와 같이 하면 된다. Thread가 죽어 버려서 먹통이 되는 경우까지 대비하려면

 

DWORD result;

result = ::WaitForSingleObject(pThread->m_hThread, 1000);   // 1초기다림

if ( result == WAIT_OBJECT_0 )

{

       // 이곳은쓰레드가확실히종료된상태임

}

else if ( result == WAIT_TIMEOUT )

{

       // 1초가지나도쓰레드가종료되지않은상태

}

 

이 방법을 사용해야 한다어떤 Thread가 현재 실행 중인지 알고 싶을 때는

 

if ( ::WaitForSingleObject(pThread->m_hThread, 0 ) == WAIT_TIMEOUT )

{

       // pThread 실행중

}

else

{

       // pThread가실행중이아님

}

 

와 같이 하면 된다.

 

User Interface Thread

 

User interface thread는 그 자체로 윈도우와 메시지 루프를 가지고 있다.

 

class CUIThread : public CWinThread

{

       DECLARE_DYNCREATE(CUIThread)

 

public:

       virtual BOOL InitInstance();

};

 

 User interface thread 독자의 윈도우도 가질  있다일반적으로 전용 Dialog 띄워

Thread 처리하는 경우가 많으므로  User Dialog CMyDialog라고 이름 지었다고 가정하면

 

IMPLEMENT_DYNCREATE(CUIThread, CWinThread)

 

BOOL CUIThread::InitInstance()

{

       m_pMainWnd = new CMyDialog;

       m_pMainWnd->ShowWindow(SW_SHOW);

       m_pMainWnd->UpdateWindow();

       return TRUE;

}

 

와 같이 CMyDialog Thread로 띄울 수 있다그 다음

 

CWinThread *pThread = ::AfxBeginThread(RUNTIME_CLASS(CUIThread));

 

와 같이 하면 MFC가 알아서 CUIThread를 생성해서 그 포인터를 pThread에 넘겨 준다.

 

아래 예제에는 CMyDialog를 띄우고 주 Process는 사용자의

입력을 기다린다. Dialog Design 및 생성은 별도로 이야기하지 않는다아래 예제를 사용하기 위해서는

CMyDialog를 만들고 ID IDD_MYDIALOG라고 가정하면 CMyDialog의 생성자에 다음과 같이 추가해야 제대로 동작한다.

 

CMyDialog::CMyDialog(CWnd* pParent /*=NULL*/)

       : CDialog(CMyDialog::IDD, pParent)

{

       Create(IDD_MYDIALOG, NULL);

}

 

이제 완전히 별도로 동작하는(Thread로 동작하는윈도우를 하나 가지는 것이다만약 이것을 Dialog가 아닌

FrameWnd라고 해도 거의 똑같다다만 위에서도 언급했듯이 Thread를 이용할 때는 Dialog가 더 일반적일 것이다.

Worker thread에 비해 훨씬 더 많은 기능을 하는 것을 알게 되었을 것이다.

 

나머지 것들은 위의 Worker Thread에 있는 내용과 동일하다.

 

만약 여러 개의 CUIThread 를 여러 개 만들려고 한다면

 

CWinThread *pThread[5];

for ( int i = 0; i < 5; i++ )

       m_pThread[i] = ::AfxBeginThread(RUNTIME_CLASS(CUIThread));

 

와 같이 하면 5개의 Thread가 생성된다.

 

Program Execution Priority(프로그램 실행 우선순위)

 

Thread 0~31까지의 priority를 가질 수 있다.

 

프로그램의 priority

 

BOOL SetPriorityClass(HANDLE hProcess, DWORD dwPriorityClass);

 

함수를 이용해서 조정할 수 있다첫 번째 인자 hProcess ::AfxGetInstanceHandle()로 얻으면 된다.

 

dwPriorityClass

Execution Priority

Description

IDLE_PRIORITY_CLASS

CPU IDLE일 때만 사용 가능

NORMAL_PRIORITY_CLASS

보통

HIGH_PRIORITY_CLASS

높은 우선 순위

REALTIME_PRIORITY_CLASS

최상위의 우선순위

 

Thread Execution Priority(쓰레드 실행 우선순위)

 

::AfxBeginThread() 함수의 nPriority를 통해서 설정하거나 CWinThread::SetThreadPriority 를 사용해 설정할 수 있다.

 

BOOL SetThreadPriority(HANDLE hThread, int nPriority);

 

nPriority

Execution Priority

Description

THREAD_PRIORITY_IDLE

REALTIME_PRIORITY_CLASS의 경우 16, 그 외에는 1

THREAD_PRIORITY_LOWEST

프로세스의 우선순위보다 2단계 낮은 우선순위를 가진다

THREAD_PRIORITY_BELOW_NORMAL

프로세스의 우선순위보다 1단계 낮은 우선순위를 가진다

THREAD_PRIORITY_NORMAL

프로세스의 우선순위가 같은 우선순위

THREAD_PRIORITY_ABOVE_NORMAL

프로세스의 우선순위보다 1단계 높은 우선순위를 가진다

THREAD_PRIORITY_HIGHEST

프로세스의 우선순위보다 2단계 높은 우선순위를 가진다

THREAD_PRIORITY_CRITICAL

REALTIME_PRIORITY_CLAS의 경우 31 그 외에는 16

 

프로그래머가 우선순위를 조정해도 Windows Scheduler가 상황에 맞게 조정하기 때문에 우선순위는

생각하고 조금 다를 수 있다.

 

Thread & Memory

 

 Thread의 지역 변수는 모두 별도로 Stack을 만들고 Local Variable들을 관리하기 때문에 위의

 

CWinThread *pThread[5];

for ( int i = 0; i < 5; i++ )

       m_pThread[i] = ::AfxBeginThread(RUNTIME_CLASS(CUIThread));

 

와 같은 경우에도 각 Thread가 다른 Thread를 침범하는 일은 없다.

 

이 쯤에서 끝났나 싶겠지만… 아직 갈 길이 멀다.

Critical section, Mutex, Semaphore 같은 것들은 다음에

 

Multithreading synchronization(멀티쓰레드의 동기화) => http://blog.naver.com/xtelite/50023359879

 

프로그램

 

Worker thread

 

#include "stdafx.h"

#include "console.h"

 

#include <iostream>

 

using namespace std;

 

#ifdef _DEBUG

#define new DEBUG_NEW

#endif

 

// The one and only application object

CWinApp theApp;

 

using namespace std;

 

UINT ThreadPrintNum(LPVOID pParam);

 

int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])

{

       int nRetCode = 0;

 

       // initialize MFC and print and error on failure

       if (!AfxWinInit(::GetModuleHandle(NULL), NULL, ::GetCommandLine(), 0))

       {

             _tprintf(_T("Fatal Error: MFC initialization failed\n"));

             return 1;

       }

 

       static BOOL bContinue = TRUE;

       CWinThread *pThread = ::AfxBeginThread(ThreadPrintNum, &bContinue);

 

       int count = 0;

       while ( count < 1000 )

       {

             count++;

       }

 

       Sleep(1000);

       pThread->SuspendThread();

       cout << "Thread suspended. Waiting for 2 seconds" << endl;

 

       Sleep(2000);

       cout << "Thread resumed" << endl;

       pThread->ResumeThread();

 

       cout << "Quit thread" << endl;

       bContinue = FALSE;

       Sleep(100);

 

       return nRetCode;

}

 

// 쓰레드함수

UINT ThreadPrintNum(LPVOID pParam)

{

       BOOL *pbContinue = (BOOL *)pParam;

       int count = 0;

       while ( *pbContinue )

       {

             count++;

       }

       cout << "Exit thread" << endl;

       return 0;

}

 

User interface thread

 

#include "stdafx.h"

#include "console.h"

 

#include "MyDialog.h"

 

#include <cstdlib>

 

using namespace std;

 

#ifdef _DEBUG

#define new DEBUG_NEW

#endif

 

// The one and only application object

CWinApp theApp;

 

using namespace std;

 

class CUIThread : public CWinThread

{

       DECLARE_DYNCREATE(CUIThread)

 

public:

       virtual BOOL InitInstance();

};

 

IMPLEMENT_DYNCREATE(CUIThread, CWinThread)

 

BOOL CUIThread::InitInstance()

{

       m_pMainWnd = new CMyDialog;

       m_pMainWnd->ShowWindow(SW_SHOW);

       m_pMainWnd->UpdateWindow();

       return TRUE;

}

 

int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])

{

       int nRetCode = 0;

 

       // initialize MFC and print and error on failure

       if (!AfxWinInit(::GetModuleHandle(NULL), NULL, ::GetCommandLine(), 0))

       {

             _tprintf(_T("Fatal Error: MFC initialization failed\n"));

             return 1;

       }

 

       CWinThread *pThread = ::AfxBeginThread(RUNTIME_CLASS(CUIThread));

      

       system("pause");

 

       return nRetCode;

}

 

반응형
반응형



CreateThread 함수 static으로 사용하지 않고 작성하기  01. VC++ Common 

2012/05/14 15:03

복사http://blog.naver.com/intra97/157643200

CreateThread 함수 static으로 사용하지 않고 작성하기 

CreateThread 함수를 사용하다 보면 하기와 같이 구성해야 한다.

1. 클래스내에서 사용시 Static 멤버함수로 작성

2. 클래스 멤버가 아닌 함수로 작성  해야만 한다.

 

그러나 클래스내 멤버 함수로 정의하여 사용하는 경우,

멤버 변수에 접근이 불가능 하지 않은가..?!!

 

그렇다고 static으로 선언하게 되면  그안에서 사용되는 함수

모두 static으로 선언해야만 한다.. 헉~!!

 

해서, 구글해보니..

 

요렇게 하면 된다..

 

static으로 ThreadProc를 선언하고, 이 함수의 인수를 this로 넘겨준다음

ThreadProc에서 인수를 클래스로 캐스팅해서 원하는 함수(static아님)를 호출하면 된다.

 

//--------------------------------------------------------

1. Thread 생성한 후

Sample::Init() {
     HANDLE hThreadID = ::CreateThread(NULL, 0, WrapThreadProc, (void*)this,

                                              0, &m_dwLAF_Thread);
     CloseHandle(hThreadID);

}

 

//---------------------------------------------------------------------
2. Thread processing for wrap
DWORD WINAPI Sample::WrapThreadProc(LPVOID lParam)
{
        Sample *test = (Sample *)lParam;
        return test ->ThreadProc(); 
}

 

//---------------------------------------------------------------------
3. Thread processing body.
bool Sample::ThreadProc()
{

     실제 제어되는 부분 ^^

}

반응형
반응형


쓰레드 중지/재시작 ( SuspendThread, ResumeThread ) 

복사http://blog.naver.com/kki2406/80041184166

SuspendThread

The SuspendThread function suspends the specified thread.

DWORD SuspendThread(  HANDLE hThread   // handle to the thread); 

Parameters

hThread
Handle to the thread.

Windows NT: The handle must have THREAD_SUSPEND_RESUME access.

Return Values

If the function succeeds, the return value is the thread's previous suspend count; otherwise, it is 0xFFFFFFFF. To get extended error information, use the GetLastError function.

 

 

ResumeThread

The ResumeThread function decrements a thread's suspend count. When the suspend count is decremented to zero, the execution of the thread is resumed.

DWORD ResumeThread(  HANDLE hThread   // identifies thread to restart); 

Parameters

hThread
Specifies a handle for the thread to be restarted.

Windows NT: The handle must have THREAD_SUSPEND_RESUME access to the thread.

Return Values

If the function succeeds, the return value is the thread's previous suspend count.

If the function fails, the return value is 0xFFFFFFFF. To get extended error information, call GetLastError.

 

DWORD WINAPI MyThread( LPVOID arg )
{
 int t = 20;
 

while ( t-- ) 
 {
  _tprintf( _T("MyThread()...%d\n"), t);
//  Sleep(500);
 }

 return 0;
}

 

int _tmain(int argc, TCHAR* argv[])
{
 int p = 20;
 DWORD idThread;

 HANDLE hThread = CreateThread(NULL, 0, MyThread, NULL, CREATE_SUSPENDED, &idThread );
 assert( hThread != NULL );
 
 while ( p-- ) 
 {
  _tprintf(_T("_tmain()...%d\n"), p);

 

  if ( p == 17 ) 
  {
   ResumeThread( hThread );
  }

 

  if( p == 10 ) 
  {
   SuspendThread( hThread );
  }

//  Sleep(500);
 }

 

 return 0;
}

 

 


 

 

http://bhnbhn.tistory.com/62

 

 

SuspendThread, ResumeThread


가끔 스레드를 쓰다보면, 스레드를 일시 정지 했다가 다시 시작하는 작업이 필요할 때가 있습니다.

그럴 때 사용하는 함수가 SuspendThread()와 ResumeThread()죠.

 

그런데 이게 특이한게,

내부적으로 suspend count 라는걸 가지고있습니다.

 

뭔얘기냐면, suspend를 할 때 마다 suspend count를 1씩 증가시키고, resume을 할 때 마다, suspend count를 1씩 감소시켜서

suspend count가 0일 때만 스레드를 돌린다는거죠.

 

즉, 10번 중지 했으면, 10번 재실행 해야 다시 돌아간다는 말씀.

 

근데, 사람 일이 어디 그런가요?

 

여기저기서 중지를 시켜놨는데, 한방에 짜잔 하고 재실행 하고 싶을 때가 있다는 말이죠.

 

으.. 그럼 어떡해야하죠? 일일이 어디서 SuspendThread가 호출되었는지 기억이라도 하고 있어야 하나요? 따로 suspend count를 기억하고있어야 하나요?

 

설마 그럴리가요.

 

간단한 해결책을 만들어봅시다.

 

void  Thread::Pause(BOOL p)
{
    if(p)
    {
        SuspendThread(m_hThread); 
    }
    else 
    {
        int supendCount;

        do{//재실행이 됨을 보장한다.
            supendCount = ResumeThread(m_hThread); 
        }while(supendCount > 0);
        
    }
}

----------------------------------------------------------------------------------------------------------

SuspendThread()스레드 사용시 유의점

보통 일시정지/시작하는 프로세스를 만드시거나
백그라운드로 돌아가는 구조를 만드는 경우 스레드를 많이 사용합니다.
물론 소켓 프로그래밍에서도 다중 처리시 사용합니다.

그런데 SuspendThread 사용시 내키는 대로 사용하면 시스템 다운되는 경우가 많습니다....
특히 외부 스레드에서 대상스레드를 정지시키는 경우 문제가 많이 일어나더군요
제가 겪은 것중에 메모리 할당하고 해제하는 루틴이 도는 중에 외부에서
스레드를 정지시켜버리는 경우... 잦은 다운이 일어나더군요~~~~

이를 피하기 위해서 외부 스레드에서 서스펜드하지 마시고...
불리언 타입 전역변수 하나 만들어서 수행중인 스레드에서 변수 체크해서
SuspendThread를 수행할것인지 말것인지 결정하면 됩니다.
ResumeThread는 물론 외부에서만 가능하지만....
고맙게도 SuspendThread는 자신의 스레드 정지가 가능합니다.~~~~

다운이 없어지더군요...


반응형
반응형




 

출처 : 원도우 사용자 그룹(http://www.aspkorea.org)


본내용은 회원으로 가입하고있는 server관련 웹모임의 운영진에서 집필된 것임으로 인용시 출처를 밝혀주시기 바랍니다.




5.6 세마포어 

세마포어는 다익스트라(Dijkstra)에 의해 제시된 개념으로, 철학자들이 굶어죽는 문제를 처음 제기한 이가 다익스트라이다. 세마포어는 병렬적인 리소스와 그 리소스를 차지하는 스레드 사이의 다대다 관계로 해석하면 된다. 세마포어는 철도의 신호기에서 온 것으로 하나의 철길을 두 대 이상의 열차가 사용하기 위해 신호를 따른다는 의미이다. 

뮤텍스가 한 번에 단 하나의 보호되는 자원을 설정할 수 있음에 반해, 세마포어는 해당 자원을 몇 개의 스레드가 동시에 사용할 수 있을지 정의한다. 일반적으로 뮤텍스를 하나의 스레드만이 접근할 수 있는 까닭에 바이너리 세마포어라고 일컫기도 하지만, 윈도우 시스템에서는 최대 하나의 스레드만이 접근할 수 있는 세마포어와 뮤텍스는 완전히 다른 개체로 인정되므로, 처음부터 뮤텍스로 설정한 것과 세마포어로 설정한 것은 차이점을 갖는다는 사실을 잊어서는 안 된다. 

뮤텍스나 세마포어는 스레드를 순서대로 동작시키는 것에 주안점을 맞추므로, 실제 CPU의 활용 상황에서 그렇게 최적화되지 못하는 경우가 많다. 하나의 리소스를 잠금 설정을 했을 경우, 해당 리소스에 복수 접근이 가능하더라도 최대 접근 수는 항상 1로 설정되기 때문에 결국 멀티스레드 프로그램에서 나타나는 복수 스레드가 특정 자원을 뮤텍스로 감쌌을 경우 여러 스레드 사이에서 대기 상태가 계속 나타나게 된다. 이는, 컨텍스트 전환 이후 아이들링 상태의 CPU 동작을 빈번히 가져오게 되며 결국 프로그램은 멀티스레드의 이점을 퇴대한 활용할 수 없다. 

세마포어는 하나의 자원, 혹은 루틴에 최대 접근 숫자를 정의해 놓고, 접근 숫자가 모두 차면 여분의 접근 숫자를 0으로 설정한다. 이후 여분 접근 숫자가 0이 아니면 그만큼의 스레드의 요청을 받아들일 수 있다. 따라서, 잘못 설계하게 되면 세마포어는 멀티스레드가 갖는 문제를 그대로 나타내게 된다. 단지 동시에 동작할 수 있는 스레드의 수만 한정해 놓는 셈이다. 따라서, 상호 간섭이 나타나는 리소스에는 세마포어를 사용할 수 없다. 가령, 열차 화장실 문제로 다시 복귀해서, 화장실이 모자라서 하나를 증설하는 경우 뮤텍스나 크리티컬 섹션으로는 동작하지 않고 세마포어로 동작하게 된다. 그러나, 화장실로 가는 스레드가 동시에 두 개까지 허용된다는 것은 실제 화장실이 두 개인 것을 의미한다. 따라서, 화장실이 하나임에도 불구하고 스레드를 두 개 만들면 어쩔 수 없이 경쟁 상태를 맞게 된다. 

세마포어가 가장 효율적으로 사용되는 것은 데이터베이스의 커넥션 등의 작업이다. 보통 데이터베이스 접근과 같은 작업들을 멀티스레딩으로 처리하는 경우가 많은데, 실제로 데이터베이스 연결은 라이선스, 혹은 성능에 의해 최대 연결 가능 숫자가 지정되어 있다. 따라서, 정의한 숫자 이내의 연결을 위해서 리소스를 사용하는 스레드를 컨트롤해야 한다. 단 하나의 리소스만을 사용할 경우에는 뮤텍스를 쓸 수 있겠지만, 리소스가 두 개 이상이 될 경우에는 효율적인 시스템 운영을 위해서 세마포어를 사용할 수밖에 없다. 

세마포어는 뮤텍스와 약간 다른 방법으로 동작한다. 뮤텍스가 전역 변수로 설정된 것에 반해 세마포어는 지역 변수로 설정된다. 세마포어는 동시에 다중 참조가 가능한 커널 객체이므로, 세마포어 이름을 알고 있다면 일단 생성되어 있는 세마포어를 열어 사용할 수 있기 때문이다. 따라서, 주 스레드에서 세마포어를 연 뒤 파생되는 스레드에서 세마포어를 사용할 수 있는 체계가 확립되는 셈이다. 

이제 뮤텍스를 사용했던 예제를 세마포어까지 확대시켜 보자. 






#include <windows.h> #include <iostream> using namespace std; DWORD WINAPI myfunction(LPVOID); int i=100; int main(){ HANDLE hThread[5]; HANDLE hSemaphore; DWORD threadId = NULL; BOOL bOK; int j; hSemaphore=CreateSemaphore(NULL, // ACL 1, // Initial Locking Count 1, // Maximum Locking Count "MySemaphore"); // 세마포어명 for(j=0;j<5;j++){ hThread[j] = CreateThread(NULL, 0, &myfunction, (LPVOID)j, 0,&threadId); if(hThread[j] == NULL) { cerr << "스레드 생성을 하지 못했습니다. :" << GetLastError() << endl; return -1; } } WaitForMultipleObjects(5, hThread, TRUE, INFINITE); //스레드 죽기 전에 세마포어가 삭제되면 안됨 bOK=CloseHandle(hSemaphore); bOK=CloseHandle(hThread); return 0; } DWORD WINAPI myfunction(LPVOID p){ HANDLE hSemaphore=OpenSemaphore(SEMAPHORE_ALL_ACCESS, FALSE, "MySemaphore"); WaitForSingleObject(hSemaphore, INFINITE); int j,k; j=(int)p; for(k=0;k<i;k++){ cout << k << endl; } long lCount; ReleaseSemaphore(hSemaphore, 1, &lCount); CloseHandle(hSemaphore); return 0; } 

전체적인 구성에 있어서는 그다지 차이를 나타내지 않는다. 세마포어는 참조가 가능한 형태로 운영되는 커널 객체이므로 일단 주 스레드에서 세마포어를 선언하고, 해당 CreateSemaphre() 함수를 사용하여 세마포어를 생성하였다. 세마포어는 참조 숫자를 갖고 있기 때문에 현재 자신이 얼마나 참조되고 있는지 ReleaseSemaphore() 함수로 확인할 수 있다. 다만 주의할 점은 ReleaseSemaphore() 함수를 사용하고 난 뒤에는 세마포어가 보호하는 루틴으로부터 빠져 나온 것이기 때문에 컨텍스트 전환의 문제를 맞이할 수 있다는 것이다. 따라서 이 참조 숫자를 사용할 필요가 있을 때 역시 컨텍스트 전환을 방지할 수 있는 방법론을 도입할 필요가 있다. 

OpenSemaphore() 함수는 세마포어를 어떻게 접근할 것인지 접근 옵션을 지정하고 있으며, 이는 일반적인 커널 객체의 경우 거의 비슷한 방법으로 접근이 가능하다. 세마포어의 경우에는 SEMAPHORE_ALL_ACCESS, SEMAPHORE_MODIFY_STATE, SYNCHRONIZE 모두 세 가지가 지원이 가능하며, 모든 접근 가능, 카운터 변경 가능, 대기 함수에서 핸들 사용 가능의 옵션이다.


 


5.7 이벤트 

이벤트는 지금까지 언급했던 동기화 객체들과는 다소 차이가 있다. 지금까지 말한 커널 객체나 크리티컬 섹션은 운영체제에 의해 영위되는 전자동식 운영이었다면 이벤트는 자동이 아니라 수동으로 운영된다. 즉 프로그래머의 의도에 의한 컨텍스트 전환이 이루어진다. 

이벤트는 일종의 전기 스위치로 볼 수 있다. 이 전기 스위치는 두 가지가 있으며, 일반적으로 보통 볼 수 있는 ON/OFF 스위치와, 작업이 끝나면 자동으로 꺼지는 스위치가 있다. 윈도우에서는 전자를 수동 재설정 이벤트(Manual Reset Event)라고 하며 후자를 자동 이벤트(Automatic Event)라고 한다. 이 두 가지의 차이점은 아주 명백하여, 수동 재설정 이벤트는 명시적으로 켜거나 끄지 않으면 항상 원래 있던 상태를 유지하고 있으며, 자동 이벤트는 항상 꺼져 있는 상태로 유지되다가, 스위치를 누르면 특정 작업을 수행하는 순간만 켜져 있고, 작업이 끝나면 도로 꺼져버리는 특성이 있다. 

이벤트 역시 핸들러로 지원되는 커널 객체이다. 따라서, 여지껏 해온 문맥대로라면 CreateEvent() 함수로 설정하고 OpenEvent() 함수로 이벤트를 열 것이다. CreateEvent() 함수에서 이벤트가 어떤 종류(수동 이벤트, 자동 이벤트)로 설정될 것인지 나타낼 수 있으며, 이벤트를 다루는 것은 다음 세 가지 추가 함수를 사용한다. 



  • SetEvent() 
  • ResetEvent() 
  • PulseEvent()

SetEvent() 함수는 이벤트를 켜는 명령어이다. 수동 이벤트라면 계속 켜져 있을 것이며, 자동 이벤트라면 단일 스레드가 작동한 뒤 다시 꺼질 것이다. ResetEvent() 명령은 이벤트를 끄는 것으로, 자동 이벤트의 경우에는 별다른 영향을 주지 못한다. PulseEvent 명령어는 자동 이벤트의 경우 SetEvent()와 동일한 효과를 나타내며, 수동 이벤트는 대기열에 있는 스레드를 동작시키게 된다.


이벤트는 프로세스 사이, 혹은 스레드 사이에서 통신할 수 있으며, 프로세스 사이에서 통신하는 것도 스레드와 동일한 코드를 사용하게 된다. 이제 질리도록 친근해진 소스를 이벤트를 사용하는 방법으로 변형해 보기로 하자. 






#include <windows.h> #include <iostream> using namespace std; DWORD WINAPI myfunction(LPVOID); int i=100; int main(){ HANDLE hThread[5]; HANDLE hEvent; DWORD threadId = NULL; BOOL bOK; int j; hEvent=CreateEvent(NULL, // ACL FALSE, // 자동 이벤트 TRUE, // 초기 상태: ON "MyEvent"); // 이벤트명 for(j=0;j<5;j++){ hThread[j] = CreateThread(NULL, 0, &myfunction, (LPVOID)j, 0,&threadId); if(hThread[j] == NULL) { cerr << "스레드 생성을 하지 못했습니다. :" << GetLastError() << endl; return -1; } } WaitForMultipleObjects(5, hThread, TRUE, INFINITE); bOK=CloseHandle(hEvent); bOK=CloseHandle(hThread); return 0; } DWORD WINAPI myfunction(LPVOID p){ HANDLE hEvent=OpenEvent(EVENT_ALL_ACCESS, FALSE, "MyEvent"); WaitForSingleObject(hEvent, INFINITE); int j,k; j=(int)p; for(k=0;k<i;k++){ cout << k << endl; } SetEvent(hEvent); CloseHandle(hEvent); return 0; } 

WaitForSingleObject()는 Event의 경우 ON 상태에서 감지된다. 따라서, 자동 이벤트를 사용하였으므로 일단 이벤트가 켜진 상태로 프로그램을 설정한다. 그러면 제일 처음 스레드가 실행되면서 이벤트가 자동으로 꺼지게 된다. 이후 발생되는 스레드는 모두 대기 상태로 들어갈 수밖에 없다. 처음 스레드가 종료되어야 SetEvent() 함수가 실행되어 다시 스레드를 원래 상태로 돌려놓을 것이기 때문이다. 

이벤트는 이처럼 특정 이벤트 객체의 상태를 파악하여 모든 작업을 제어한다. 따라서, 상대적으로 단순한 구조를 갖고 있다. 또한 ACL을 올바르게 설정하였다면 다른 스레드는 물론 프로세스에서도 해당 이벤트를 같은 이름으로 받을 수 있다. 다만, 프로세스 사이에서 통신이 이루어질 경우에는 이벤트 이름 형식을 정확히 정의해 주어야 한다. 가령 다음과 같이 하면 될 것이다. 


   HANDLE hEvent=OpenEvent(EVENT_ALL_ACCESS, FALSE, _T("MyEvent")); 

5.8 대기 타이머(Waitable Timer) 및 타이머 큐(Timer Queue) 

지금부터 언급하는 내용들은 근본적으로 위에서 언급된 루틴과는 다르다. 아예 Sleep()과 같은 함수와 동일한 개념으로 동작한다고 생각하면 된다. 즉, 일정 시간의 유예를 둔 뒤 해당 시간이 지나면 자동으로 해당 작업을 멈추어 버리는 방법이다. 타이머는 뮤텍스나 세마포어처럼 명시적으로 스레드 동기화에 관여하지 않으므로, 실제로는 동기 함수라기 보다는 특별한 경우에만 주로 사용된다. 이는 Sleep() 함수보다 더 시스템을 효율적으로 다룬다는 특징을 지닐 뿐이다. 따라서 Sleep()이 필요한 시점에서 대기 타이머나 타이머 큐를 사용하는 것을 권한다. 

타이머 큐는 단순히 일정 시간을 기다리는 콜백 함수이며, 대기 타이머는 단순히 기다리는 것이 아니라, 이벤트와 같이 ON/OFF 등을 구분하는 상태를 나타낼 수도 있고, 특정 시간이 지나면 주기적으로 활성화되는 형태를 나타내기도 한다. 대기 타이머 역시 이벤트와 비슷하게 수동 타이머(Manual Reset Timer)와 동기 타이머(Synchronization Timer)가 존재한다. 동기 타이머가 이벤트의 자동 타이머와 비슷한 구실을 하며, 그 외에 주기 타이머(Periodic Timer)가 있다. 타이머 역시 생성과 소멸은 비슷한 문법을 가지므로, CreateWaitableTimer() 함수로 생성된다. 문법은 거의 이벤트와 흡사하며, 동기화에서 그렇게 많이 사용되지 않으므로 더 자세한 내용을 원하는 이들은 MSDN에서 사용법을 참고할 것을 권한다.


 


5.9 TLS 

스레드에서 전역 변수를 사용할 경우 모든 스레드가 해당 변수를 건드리기 때문에, 다른 스레드가 전혀 동작하지 않는다고 하더라도, 컨텍스트 전환의 결과로 특정 데이터를 엉망으로 만들어 버릴 가능성이 높다. 이 경우 스레드 로컬 저장소를 사용하면 문제가 깔끔히 해결된다. 스레드 로컬 저장 영역은 32비트 블록으로 지정되며, 각각의 스레드별로 사용 공간을 할당할 수 있다. 포인터로 값을 전달하기 좋아하는 C 계열의 프로그래머들이 일반적인 멀티스레드 프로그램을 작성할 때, 스레드와 해당 스레드가 호출하는 함수 사이에서 인수 전달이 포인터로 이루어진다면 여러 문제가 발생할 수 있다. 

제일 큰 문제는 포인터가 전역 변수로 지정되어야 한다는 것이다. 이 말은 컨텍스트 스위칭이 일어나지 않더라도, 각각의 멀티스레드 프로그램이 포인터로 지정된 변수를 일그러뜨릴 가능성이 매우 높다는 것이다. 가령, 항상 초기값이 0으로 세팅되어 있어야 특정 스레드와 그 연관 함수가 만족하지만, 스레드 수행 결과 그 포인터 변수는 초기값이 스레드의 결과값으로 치환된 이후 컨텍스트 전환이 일어난 경우 다른 스레드는 엉뚱한 초기값을 받게 된다. 물론, 이러한 부분은 뮤텍스나 세마포어 등을 사용하여 초기값을 항상 원래대로 복구하는 것으로 해결할 수 있다. 

그렇지만, 뮤텍스나 세마포어는 비교적 수행 속도가 느리고, 경쟁 상태나 교착 상태의 가능성이 없이 단순히 변수의 변화를 막기 위해 뮤텍스나 세마포어를 사용하는 것은 비효율적이다. 또한 스레드 동기화는 근본적으로 컨텍스트 전환은 계속 수행하고 있는 상황이기 때문에 CPU의 시간을 계속 소모하면서 대기하는 방법을 사용하게 된다. 따라서, 스레드와 그 스레드가 호출하는 함수 사이에서만 사용할 수 있는 포인터 변수를 도입하면 메모리 측면에서는 모든 참조형 변수의 사본을 만드는 손해를 볼 수 있지만, 각각의 스레드를 병렬적으로 수행하면서 컨텍스트 전환에 의해 발생하는 문제들을 근원적으로 해결할 수 있다. 경쟁 상태가 나타나지 않는, 단순히 멀티스레드의 이점을 최대한 활용하기 위해 이러한 방법론을 사용한다. 물론 저장 영역을 손해보고, 메모리 연산을 빈번히 사용하기 때문에 복잡해지는 경우 오류 가능성이 더욱 커질 수 있다. 

TLS는 각 스레드를 위한 전용 주소 공간을 제공하며, 각 주소 공간의 인덱스로 해당 공간을 접근한다. 이는 마치 지하철 물품보관소와 같이 각자가 물품보관소 열쇠를 갖고 있고, 해당 열쇠를 가진 사람만이 물건을 넣고 꺼낼 수 있는 형태로 운영된다. 각 스레드는 호출되는 함수를 통해 자신이 삽입한 변수를 공유할 수 있다. TLS는 정적, 혹은 동적으로 할당될 수 있으며, 마치 일반적인 C++ 프로그램의 메모리 할당과 비슷한 방법으로 동작하게 된다. 다음 그림은 TLS와 스레드의 관계를 나타낸 것이다. 


 


각각의 스레드는 TLS 영역에 보관함을 가질 수 있으며, 이 스레드는 자신이 호출한 함수, 혹은 자신 내에서만 이 TLS 영역에 접근할 수 있다. 빈번히 사용되는 동적 TLS는 TlsAlloc() 함수로 저장 공간을 설정하며, TlsFree() 함수로 저장 공간을 해제한다. 저장 공간에 값을 입력할 때는 TlsSetValue() 함수로 값을 입력하고, 출력은 TlsGetValue() 함수로 처리한다.


다음 코드는 TLS를 동적으로 사용한 예제이다. 






#include <windows.h> #include <iostream> #include <cstdio> using namespace std; DWORD WINAPI myfunction(LPVOID); VOID CommonFunc(VOID); DWORD dwIndex; int main(){ HANDLE hThread[5]; DWORD threadId = NULL; BOOL bOK; int j; dwIndex=TlsAlloc(); //Tls 영역 설정 for(j=0;j<5;j++){ hThread[j] = CreateThread(NULL, 0, &myfunction, (LPVOID)j, 0,&threadId); if(hThread[j] == NULL) { cerr << "스레드 생성을 하지 못했습니다. :" << GetLastError() << endl; return -1; } } WaitForMultipleObjects(5, hThread, TRUE, INFINITE); bOK=CloseHandle(hThread); TlsFree(dwIndex); //Tls 영역 해제 return 0; } DWORD WINAPI myfunction(LPVOID p){ int j; j=(int)p; int *m=new int; *m=j+1; TlsSetValue(dwIndex, (LPVOID)m); //변수를 TLS Index를 받고 TLS 영역에 삽입한다. CommonFunc(); return 0; } // 스레드와 함수 사이에서는 *m을 공유하지만, 이는 각각 스레드별로 독립적으로 행동한다. VOID CommonFunc(VOID) { int i, *m, k; m=(int *)TlsGetValue(dwIndex); //호출된 for(i=0;i<*m;i++) k=*m+i; cout << "k=" << k << "(Thread:" << GetCurrentThreadId() << ")" << endl; } 

이 코드는 스레드 동기화를 위해 어떠한 방법론도 사용하지 않고 있다. 출력 화면에서 경쟁 상태가 나타나기는 하겠지만, 실제로 프로그램의 수행 결과는 예측대로 나타나게 된다. 

정적 TLS는 조금 더 다루기 쉽다. 동적 TLS가 전역 메모리 할당과 흡사하게 동작한다고 하면 정적 TLS는 전역 변수 선언과 비슷하게 선언된다. 앞선 동적 할당이 주로 포인터 변수를 처리하기 위한 방법으로 사용된다면, 정적 TLS는 전역변수가 항상 동일한 초기값을 갖도록 처리할 수 있다. 정적 TLS를 사용하는 방법은 단지 전역 변수를 선언할 때 __declspec(thread)키워드를 사용하면 된다. 다음 코드를 한 번 살펴보자. 






#include <windows.h> #include <iostream> #include <cstdio> using namespace std; DWORD WINAPI myfunction(LPVOID); VOID CommonFunc(VOID); DWORD dwIndex; __declspec(thread) int m=1; // TLS 전역 변수 사용 int main(){ HANDLE hThread[5]; DWORD threadId = NULL; BOOL bOK; int j; for(j=0;j<5;j++){ hThread[j] = CreateThread(NULL, 0, &myfunction, (LPVOID)j, 0,&threadId); if(hThread[j] == NULL) { cerr << "스레드 생성을 하지 못했습니다. :" << GetLastError() << endl; return -1; } } WaitForMultipleObjects(5, hThread, TRUE, INFINITE); bOK=CloseHandle(hThread); return 0; } DWORD WINAPI myfunction(LPVOID p){ int j; j=(int)p; CommonFunc(); m=m+j; // 만약 일반적인 전역 변수라면 이 구문이 호출된 이후 변수 자체가 변할 것이다. return 0; } // 스레드와 함수, 그리고 전체 코드는 m을 공유하지만, m은 생성된 스레드에 따라 다른 변수로 // 취급받는다. VOID CommonFunc(VOID) { int i, k; for(i=0;i<m;i++) k=m+i; cout << "k=" << k << "(Thread:" << GetCurrentThreadId() << ")" << endl; } 

이 프로그램이 초기 선언된 m 값을 일반적인 변수로 선언했다면 스레드에서 m 값을 계속 변경시키기 때문에 전역 변수는 각각의 스레드마다 다른 초기값을 받게 되며, 출력 결과는 예측할 수 없도록 변할 것이다. 그러나, 정적 TLS 변수로 선언되었기 때문에 모든 변수는 스레드 내에서 같은 초기값을 갖게 된다. 

정적 TLS를 사용할 때 TLS 인덱스는 명시적으로 생성되거나 파괴되지 않는다. 스레드가 생성될 때 TLS 저장 영역이 생성되며, 스레드가 파괴될 때 같이 파괴된다. 주로 멀티스레드 프로그램을 수행하면서 정적 초기 변수가 변경될 가능성이 있지만 이러한 변수들을 제외하고는 교착 상태나 경쟁 상태가 벌어지지 않을 경우 TLS는 유용하다.


 


6.1 스레드 우선권 

윈도우 시스템은 스레드를 기준으로 다중 작업이 이루어지는 멀티스레딩 프로그램이라고 앞에서 언급하였다. 스레드는 각각 일정 시간의 실행 시분할을 갖게 되며, 일단 동시에 모든 요청 스레드를 다 처리할 수 없으므로, 순서대로 처리하는 스레드 큐(Thread Queue)가 형성되게 된다. 그런데, 이 큐가 한 줄로 늘어선 것이 아니라, 우선권이라는 개념을 가진 상태에서 병렬적인 큐로 형성되어 있다. 우선권은 숫자가 커지면 커질수록 우선적인 개념을 갖고 있고 이는 시스템이 빨리 처리해야 하는 스레드이다. 

가령, 은행 객장에 우대고객 전용 창구가 따로 있고, 아무리 은행 줄이 길어도 이 우대고객 전용 창구에 줄을 선 우대고객은 가장 먼저 은행일을 볼 수 있는 것과 같은 개념이다. 윈도우는 이 우선권이 31번까지 있다. 따라서, 스레드가 어떤 큐에 들어가느냐에 따라 처리되는 속도가 달라지게 된다. 다음 그림은 스레드가 각각의 큐에 들어가 있는 모습이다. 


 


우선권이 높은 큐를 할당받는 스레드는 주로 I/O 등이나 시스템과 관계된 중요한 시스템들이다. 엄밀하게 말해서 우선권이 높은 큐는 그만큼 많은 시간 할당을 받게 된다. 시분할 멀티태스킹이기 때문이다.


윈도우 시스템은 동적으로 우선권을 관리한다. 이는, 프로그램이 실행되는 동안에도 계속 우선권이 변경될 수 있으며, 시스템은 항상 우선권을 적절한 방법으로 통제하고 관리한다. 가령, 윈도우 NT 이상의 시스템에서 나타나는 성능 옵션에서, 포그라운드 서비스와 백그라운드 서비스의 응용 프로그램 응답 옵션을 바꿀 수 있다. 참고로 윈도우 시스템은 유닉스 시스템과 포그라운드 서비스와 백그라운드 서비스의 개념상의 차이가 존재한다. 윈도우에서는 로그온 여부와 프로세스의 생존이 바로 직결되어 있으므로 로그오프 이후에도 운영되는 프로그램은 존재하지 않는다. 어떤 방법이든 사용자 로그온이 처리되어 있어야 프로세스가 생존할 수 있으며, 이렇게 따진다면 유닉스 개념의 포그라운드와 백그라운드가 나타나지 않는다. 

윈도우 시스템에서 백그라운드 서비스는 현재 활성화되지 않은 모든 다른 프로세스를 의미한다. 즉, 로컬 로그온이 처리되어 창의 상단의 바가 파란색으로 바뀌지 않은 다른 모든 프로세스는 백그라운드 서비스이다. 이 경우, 성능 옵션에서의 다음 조정은 결국 우선권의 조정일 수밖에 없다. 


 


실제로 위와 같은 성능 옵션의 응용프로그램 최적화는 원래 이 응용프로그램이 갖도록 정의되었던 스레드의 우선권보다 다소 높은 우선권을 동적으로 설정하라는 것이다. 우선권의 개념은 작업 관리자에서도 볼 수 있으며, 작업 관리자에서는 우선권 클래스에 해당하는 다섯 가지 개념이 나타나게 된다. 이 우선권들은 일차적으로 프로세스가 형성될 경우 지정된다. IDLE(4), NORMAL(7,9 또는 7+1=8, 9+1=10), HIGH(13), REALTIME(24)가 일차적으로 부여되며, 이 값에 가감을 하여 우선권 적용이 이루어진다. 가감은 +4~-3까지 가능하며, 이 조합으로 전체 우선권을 만들어 낼 수 있다. 따라서 우선권은 총 32개가 있기는 하지만 여기서 만들어 낼 수 있는 것은 총 21개가 된다. 이 중 가감이 될 수 없는 절대값(1, 15, 31)이 존재하며, 포그라운드 성능 최적화를 한 경우 초기부터 9-10 사이의 우선권이 주어진 채로 출발하게 된다. 음영을 넣은 부분은 실시간(Real Time) 우선권에 해당하는 부분이며, 일반적으로 사용자 응용프로그램의 우선권이 15 이상으로 올라가는 경우는 거의 없다. 만일 사용자 프로그램의 우선권이 15 이상으로 올라간다면 이는 프로그램상의 버그이거나 프로그래머가 의도적으로 실시간 큐에 삽입한 것이며, 이런 상황에서 프로그램의 응답이 지연되는 경우 전체 시스템이 같이 느려지는 현상이 벌어진다. 이는 상위 큐에 시간 할당이 더 많이 되어 있으며, 별다른 이유 없이(콜백 함수나 객체 상태 함수를 사용하지 않고) 시스템이 느려진다면 CPU 자원을 계속 잡아먹고 있는 셈이 된다. 15 미만에서 시작한 스레드의 우선권 상한도는 15까지이며, 15 이상에서 시작한 우선권이 상한선 31을 갖게 된다. 실제로 이렇게 높은 우선권을 갖고 있는 경우는 찾기 어렵다. 윈도우 CE의 경우 우선권은 32가지를 모두 사용하며, 이러한 측면들은 윈도우 소프트웨어가 발전할수록 계속 변하는 부분이다. 다음 표는 윈도우 스레드 우선권을 전부 나열한 것이며, 어떤 우선권이 있으며, 어떻게 우선권 변경이 되는지 확인해 볼 수 있다. 15, 31 은 절대값이며, 더 이상 가감되지 않는다.



 


6.2 우선권 가감


스레드의 우선권 가감 연산은 누적하여 수행할 수 없다. 운영체제는 하나의 스레드에 대한 기저 우선권(프로세스가 시작되는 시점에서의 우선권, Base Priority)을 기억하고 있으며, 스레드의 우선권 조정 함수는 이 기저 우선권에 대해서만 가감을 한다. 즉, 원래 기저 우선권이 10인 스레드에 우선권 조정을 몇 번 반복하더라도 우선권은 12 또는 15로밖에는 설정할 수 없다는 것이다. 따라서, 우선권은 사용할 수 있는 범위 한계가 있다. 다음과 같이 프로그램을 작성하고 컴파일해 보자. 






#include <windows.h> #include <iostream> using namespace std; int main(){ HANDLE hThread; char a; hThread=GetCurrentThread(); cin >> a; // 잠시 정지를 위함 SetThreadPriority(hThread, THREAD_PRIORITY_HIGHEST); cin >> a; SetThreadPriority(hThread, THREAD_PRIORITY_HIGHEST); cin >> a; SetThreadPriority(hThread, THREAD_PRIORITY_HIGHEST); cin >> a; return 0; } 

이 프로그램은 현재 동작하는 스레드(주 스레드)로부터 핸들을 얻어, 자신 스레드의 우선 권을 변경하는 프로그램이다. GetCurrentThread() 함수는 현재 동작하는 스레드의 핸들을 반환하며, SetThreadPriority() 함수는 현재 스레드의 우선권을 변경한다. 초기 기저 우선권은 프로세스가 형성될 때 생성되며, 별다른 옵션 없이 프로세스를 생성하였다면 기저 우선권은 운영체제가 적절하게 정의한다. 

이 프로그램은 기저 우선권에 우선권을 계속 더하는 기능을 하고 있다. THREAD_PRIORITY_NORMAL은 +2를 하는 키워드이며, 이 프로그램을 수행하면서 성능 모니터를 같이 띄워 스레드의 우선권이 어떻게 변하는지 한 번 확인해 보기 바란다. 만일 기저 우선권이 8이었다면 10, 12, 14 이렇게 계속 증가할 것인지, 아니면 8+2 만 계속 반복하여 결과는 항상 10이 나오는지 측정해 보면 된다. 

각자의 컴퓨터는 모두 다 다른 설정을 지니므로, 기저 우선권은 1~2 정도 오차가 있을 수 있다. 따라서, 프로그램의 수행 결과 초기값에 비해 어느 정도의 변경이 있었는지를 알아내는 것이 중요하며, 컴파일해서 실행해보면 알겠지만, 기저 우선권에 비해 결코 2 이상의 값이 올라가지 않는다. 시스템은 해당 스레드의 기저 우선권을 기억하고 있고, 해당 명령을 계속 수행하면 기저 우선권에 계속 2를 더하게 되는 까닭이다. 

우선권의 변경은 일반적으로는 운영체제의 필요에 의해 이루어진다. 이 말은 운영체제의 필요가 아닌 경우라면 우선권의 변경은 프로그램적으로는 거의 이루어지지 않는다는 것이다. 간혹 인터넷 익스플로러 등의 프로그램이 작업 관리자의 종료 명령도 무시하고 계속 수행되는 경우를 볼 수 있으며, 이런 현상은 프로그램의 버그 등에 의해 작업 관리자 보다 해당 프로그램의 스레드 우선권이 높은 경우에 나타나게 된다.


 


7.1 윈도우와 프로세스 

유닉스에서는 여러 개의 프로세스를 동시에 운영하는 것이 보편적이었다. 분기(fork)라는 형태로 하나의 프로세스에서는 다른 프로세스를 형성하고, 그 기저에는 셸 프로세스가 존재한다. 일차적으로 셸 프로세스가 생성되고 그 위에서 프로세스는 계속 시스템 분기를 사용하여 프로세스를 형성해 나간다. 만들어진 프로세스는 이전 프로세스의 거의 대부분의 속성을 계승하며, 특히 보안성과 관련되어서는 출발한 프로세스의 대부분의 속성을 갖고 있다. 

프로세스는 해당 프로그램에서 필요한 주소 공간을 확보하고 있으며, 관계 리소스를 모두 보유하고 있다. 유닉스에서는 fork() 함수를 사용하여 시스템 호출로 새로운 프로세스를 만들어 내며, 프로세스 자체가 차지하는 오버헤드가 그렇게 크지 않기 때문에 멀티프로세스 프로그램이 힘들지 않게 운영된다. 

윈도우 시스템에서는 런타임 라이브러리 레벨의 _spawn() 혹은 _exec() 함수를 사용하거나, Win32 API 레벨의 CreateProcess() 함수를 사용할 수 있다. CreateProcess() 함수는 상당히 많은 인수를 갖고 있으며 그 대부분은 프로세스에 대한 미세 조정이다. 

주의할 점은 윈도우에서는 프로세스를 여러 개 생성시키는 형태의 작업은 거의 이루어지지 않으며, 필요에 의해 프로세스를 생성한다고 할지라도 최소한의 프로세스 작업으로 처리해 주어야 한다. 윈도우 프로세스는 새로 프로세스를 형성하였을 경우에는 기존 프로세스와 완전히 별개인 하나의 프로세스가 형성된다. 실제로 윈도우 시스템에서 새로운 프로세스를 형성하여 작업을 시도하는 경우는 그렇게 많지 않다. 동시에 많은 창이 뜨는 윈도우 프로그램은 거의 다 새로운 창은 하나의 스레드로 동작하도록 설정한다. 프로세스와 셸을 가득 띄워 놓은 유닉스 프로그램에 익숙하다면 이렇게 동작하는 방법론이 다소 답답할 수도 있을 것이다. 

작업 관리자를 사용하면 현재 운영되고 있는 프로세스와 프로세스가 사용하고 있는 CPU의 시간 점유율을 볼 수 있다. 프로세스가 사용하는 시간 점유율은 그 프로세스가 형성한 모든 스레드의 동작 시간의 총합으로 정의된다. 스레드를 전혀 생성하지 않도록 프로그램을 설계했다고 하더라도 최소 하나의 스레드가 정의된다고 앞에서 이미 언급하였다. 

실제로 프로세스를 생성하는 것은 아주 다양하고 복잡한 개념을 포괄하고 있다. 기본적으로 윈도우 2000은 보안성이 강한 운영체제이며, 이에 따라 어떤 스테이션, 어떤 세션, 그리고 어떤 ACL을 갖고 있는지 프로세스 자체에 명시되어야 한다. 일상적으로 이러한 작업들은 유닉스와 비슷하게 모 프로세스의 사본을 사용하는 경우가 많지만, 윈도우에서는 근본적으로 이런 설정들을 아예 처음부터 “새로” 만들 수 있다는 사실을 주목하자. 프로세스와 스레드 자체는 윈도우 보안성과 아주 밀접히 관련되어 있으며, 이런 일들은 시스템에 누가 들어와 있는지 판별하는 과정과 아주 밀접하게 관련된다. 

7.2 프로세스 생성 



프로세스 생성을 위해서는 BOOL 형의 CreateProcess() 함수가 사용된다. 이 함수는 생각보다 인수가 많으며, 각각의 인수들 중 일부는 구조체로 정의되어 있어 전부를 이해하는 것이 그렇게 생각보다 쉽지는 않다. 또한 보안성이라는 것이 같이 맞물려 있어, 해당 프로세스를 어떻게 형성할 것인지에 대해 정의할 수 있다.


윈도우는 다양한 종류의 프로세스가 존재한다. 여기서 Win32 API에 한정한다면 16비트 코드를 해석할 수 있는 WOW(Windows On Win32) VDM(Virtual DOS Machine) 내에 프로세스를 한정시킬 것인지, WOW VDM도 공유 VDM을 쓸 것인지 전용 VDM을 쓸 것인지 고려해야 하는 문제가 벌어진다. 하나의 프로세스에서 다른 프로세스를 형성할 때 해당 프로세스가 혹시 Win16 코드를 갖고 있지 않는지 한 번쯤 살펴볼 필요가 있다. 윈도우 XP가 나오는 이 시점에서 Win16 코드를 논하는 것이 시대착오적인 것 같지만 실제로 중소규모 소프트웨어 회사에서 제작한 아주 많은 애플리케이션은 여전히 Win16 코드를 일부 갖고 있다. 

다음은 아주 간단한 프로세스 생성의 예이다. 이 코드는 인터넷 익스플로러를 실행시키고 모 프로세스는 중단된다. 






#include <windows.h> #include <iostream> using namespace std; int main(){ STARTUPINFO stInfo; PROCESS_INFORMATION pInfo; BOOL bOk; LPTSTR strPath="C:\\Program Files\\Internet Explorer\\iexplore.exe"; ZeroMemory(&pInfo, sizeof(PROCESS_INFORMATION)); ZeroMemory(&stInfo, sizeof(STARTUPINFO)); stInfo.cb=sizeof(STARTUPINFO); bOk = CreateProcess(0, strPath, NULL, NULL, FALSE, CREATE_DEFAULT_ERROR_MODE, NULL, NULL, &stInfo, &pInfo); if(!bOk){ cout << "프로세스 생성 오류:" << GetLastError() << endl; return -1; } WaitForSingleObject(pInfo.hProcess, INFINITE); // 생성한 프로세스가 종료될 때까지 기다린다. CloseHandle(pInfo.hProcess); CloseHandle(pInfo.hThread); return 0; } 

CreateProcess() 함수는 꽤 많은 인수를 갖고 있으며, MSDN에 이에 대해서는 아주 상세히 설명되어 있으므로 MSDN을 참조하기 바란다. 프로세스를 생성하는 것은 크게 네 가지 중요 정보를 제공해 준다. 일단 CreateProcess()가 받아들이는 인수는 다음과 같다. 

BOOL CreateProcess( LPCTSTR lpApplicationName, LPTSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCTSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation ); 

보안 옵션은 사용자그룹 보안 분과에서 해당 내용을 찾아볼 수 있을 것이다. 생성 플래그는 몇 가지 방법론을 제공하며, 이 플래그를 어떻게 조작하느냐에 따라 16비트 VDM 혹은 디버그 이벤트 받음 여부 등을 설정할 수 있다. 또한 생성된 프로세스의 주 스레드를 대기 모드(suspended state)로 생성할 수도 있다. 프로세스를 생성하기 위한 환경 변수로 STARTUPINFO 구조체를 사용하며, 프로세스가 생성한 스레드 및 프로세스의 핸들을 받기 위해서 PROCESS_INFORMATION 구조체를 사용하게 된다. 환경 변수는 초기 실행이 고정되지 않은 프로세스를 대상으로 프로세스의 시작 위치, 윈도우 크기 등을 지정할 수 있으며, 프로세스 정보 구조체에서는 생성된 프로세스와 주 스레드의 ID 및 핸들을 얻을 수 있다. 

뒤에서 다루겠지만, 윈도우에서 프로세스가 새 프로세스를 생성할 때는 기존 프로세스의 속성을 계승할 수 있도록 설정하기도 하고, 계승하지 않도록 설정하기도 한다. 다음 제시되는 함수는 CreateProcess() 함수와 생긴 모양은 흡사하지만, 기존 프로세스와는 다른 사용자 계정에서 운영되는 새 프로세스를 형성한다. 이것은 4장에서 상세히 다루어질 것이다. 

BOOL CreateProcessAsUser( HANDLE hToken, LPCTSTR lpApplicationName, LPTSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCTSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation ); 

7.3 프로세스 종료 및 정보 얻기


유닉스에서는 전통적으로 kill 명령어를 사용하여 프로세스를 강제로 종료하였다. 윈도우 시스템에는 작업 관리자가 존재하지만 작업 관리자는 반드시 GUI로 실행해 주어야만 되는 단점이 존재하고 있다. 그런데 윈도우에서 프로세스를 종료하는 것은 그다지 어려운 작업이 아니다. 

프로세스 역시 커널 객체이므로, 이제 커널 객체에 익숙해졌다면 생성된 프로세스 ID로 프로세스 핸들을 받아 와서 해당 핸들을 가진 프로세스를 종료하면 된다는 등식이 머릿속에 도식적으로 나타날 것이다. 

그런데 윈도우 프로세스는 일반 객체가 아니라 보안성까지 체크되는 보안 객체이다. 이러한 까닭에 단순히 프로세스 번호로 해당 프로세스를 종료시키는 것이 아니라, 해당 프로세스의 ACL 혹은 시스템 프로세스 여부 등을 파악하여 최종적으로 종료시킬 수 있는지를 판단한다. 사용자가 충분한 권한이 있으며, 해당 프로세스를 종료시킬 수 있다면 얻어낸 핸들값을 사용하여 프로세스를 완전히 종료시킬 수 있다. 

시스템 프로세스의 경우 작업 관리자를 통해서도 액세스 거부가 나타나며, 이는 보안 분과에서 이유를 알아보도록 하라. 작업 관리자 등을 사용하여 알아낸 특정 프로세스를 종료하기 위한 코드는 다음과 같다. 






#include <windows.h> #include <iostream> using namespace std; int main(int argc, char* argv[]){ if (argc !=2) { cout << "사용법: kill Pid" << endl; return -1; } int processId=atoi(argv[1]); unsigned uExitCode=0; HANDLE hProcess; hProcess=OpenProcess(PROCESS_TERMINATE, FALSE, processId); if(TerminateProcess(hProcess, uExitCode)) cout << "프로세스 " << processId << "번을 종료했습니다." << endl; else cout << "종료하지 못했습니다." << endl; return 0; } 

파일이나 기타 핸들을 사용하는 여러 구문들은 전부 이와 비슷한 형태로 동작하게 된다. 만일 프로세스를 종료하지 못했다면 프로세스가 시스템에 소속된 것이거나, ACL이 맞지 않아 종료할 수 없는 것일 가능성이 높다. 

프로세스를 중단하기 위해서 TernminateProcess() 함수를 주로 사용하지만, 간혹 프로세스가 자살할 필요가 있다. 보편적으로 윈도우 프로그램은 명시적으로 프로세스를 종료하라는 말이 없을 경우에는 프로세스를 종료하지 않으므로 ExitProcess() 함수를 사용하여 종료할 수 있다. 이 함수는 자살을 위해 사용하므로 void 형이면 충분하다. 

ExitProcess() 함수는 사용중인 DLL에 대한 참조값을 제거하고 발생시킨 모든 스레드의 참조값 역시 제거한다. 그리고 스스로 죽고 싶다는 메시지를 객체 관리자에게 송신한다. 그러나, 객체 관리자가 “죽여” 주기 전까지는 실제로 프로세스 객체가 사라지는 것은 아니다. 이는 앞에서 언급하였다.


 


8.1 파이버 

파이버는 스레드보다 더 작은 단위의 일을 의미한다. 보통 SQL 서버를 운영하면서 파이버라는 단어를 처음 볼 수 있다. 다음과 같은 정보 창에서 윈도우 파이버를 이용하는 체크박스를 한 번쯤 보았을 것이다. 


 


멀티 스레딩 프로그램에서 동시에 엄청난 다중 작업이 들어왔을 경우, 시스템은 컨텍스트 전환 오버헤드가 걸리게 된다. 특히 다중 프로세서를 운영하는 중앙 데이터 서버와 같은 경우는 초당 수천 건의 컨텍스트 전환과 100%에 육박하는 CPU 점유율을 보이는 경우가 다반사이다. 이러한 경우에 특화된 해결책이 파이버이다.


스레드(Thread)가 실을 의미하고 있기 때문에 실의 원료가 되는 섬유(fiber)로 이름을 붙인 것으로 생각된다. 파이버는 스레드와 달리 컨텍스트 전환을 시스템의 스케쥴에 따르지 않고 수동으로 해 주어야 한다. 따라서, 컨텍스트 전환에 따른 오버헤드가 발생하지 않는다. 그런데, 컨텍스트 전환을 수동으로 하는 것은 결국 멀티스레딩의 강점을 상당 부분 잃어버리는 것을 의미한다. 따라서, 파이버로 모든 코드를 작성하는 경우는 존재할 수 없다. 이는 프로세스 단위에서 항상 싱글스레딩을 영위하고 있는 결과를 낳게 된다. 

만일 파이버가 스레드에서 파생될 수 있다면 어떤 의미를 지닐까? 이는 컨텍스트 전환은 스레드 단위에서 이루어지고, 업무는 파이버 단위에서 이루어지는 것을 의미한다. 가령 100개의 스레드를 사용하는 응용프로그램이 존재한다고 할 때, 스레드 100개를 영위하기 위해서는 100가지의 서로 다른 상태를 저장하고, 이들 사이에서 전환이 이루어져야 하지만, 50개의 스레드와 2개의 파이버를 사용한다면 컨텍스트 전환에 따르는 오버헤드를 다소 줄일 수 있다 시스템에서 이루어지는 컨텍스트 전환은 스레드 단위이기 때문에 각각의 스레드는 어차피 할 컨텍스트 전환이 반으로 줄어드는 셈이 된다. 그리고, 파이버 상호간에 이루어지는 컨텍스트 전환은 그 오버헤드도 작고 수동으로 이루어지므로 많은 장점이 존재한다. 

그러나 스레드 대신 파이버를 사용하는 것은 성능 계산을 정확하게 해야 하는 단점이 존재한다. 따라서, 대용량 멀티스레드 프로그램에서 주로 채용되며, 현재까지 가장 효과적으로 사용되고 있는 것은 멀티프로세서가 장착된 MS-SQL 데이터베이스 서비스이다. 파이버를 사용할 경우 가장 큰 약점은 자기 자신의 프로세스가 전체 CPU를 대부분 장악하는 상황이 아니라면 컨텍스트 스위칭에서 후순위로 밀려버리는 상황이 벌어질 수 있다는 것이다. 즉, 스레드 숫자가 적기 때문에 할당에 따른 상대적 차별을 당할 수 있다. 따라서, SQL Server의 경우 CPU 점유량이 많지 않다면 파이버로 스레딩을 대신하는 것은 아무런 득이 되지 못할 가능성이 높다. 

다음은 파이버를 사용하는 예제 코드이다. 






#define WIN32_LEAN_AND_MEAN #define _WIN32_WINNT 0x0400 #include <windows.h> #include <iostream> using namespace std; void WINAPI myfunction(LPVOID); int i=100; LPVOID LPFiber[5]; int main(){ int j; for(j=0;j<5;j++){ LPFiber[j] = CreateFiber(16384, &myfunction, (PVOID)j); if(LPFiber[j] == NULL) { cerr << "파이버 생성을 하지 못했습니다. :" << GetLastError() << endl; return -1; } } ConvertThreadToFiber(0); //주 스레드를 파이버로 변신시킨다. SwitchToFiber(LPFiber[0]); // 첫번째 파이버로 컨텍스트 전환을 시도한다. for(j=0;j<5;j++){ DeleteFiber(LPFiber[i]); //파이버를 지워 할당된 자원을 해제한다 } return 0; } void WINAPI myfunction(LPVOID p){ int j,k; j=(int)p; for(k=0;k<i;k++){ cout << k << endl; } if(j < 4) SwitchToFiber(LPFiber[j+1]); } 

파이버는 컨텍스트 전환에 따른 동기 문제가 발생하지 않기 때문에 동기 관계 객체들이나 이벤트 등을 사용할 필요가 전혀 없다. 다만 생성된 파이버 상호간 수동으로 전환하는 코드를 삽입하여야 한다. 스레드가 DWORD형의 함수를 갖는 데 반해 파이버는 VOID 형이라는 차이가 있다. 일단 전역 파이버를 선언한다. 그리고, 각각의 파이버를 생성한다. 다른 객체와 마찬가지로 파이버를 생성하는 것도 CreateObject 형식의 CreateFiber() 함수를 사용한다. CreateFiber()함수는 파이버에 할당될 초기 스택 크기와 함수 포인터, 그리고 전달 인수를 받아들인다. 파이버 당 할당될 수 있는 초기 최대 스택 크기는 따로 정의하지 않는 한 1MB이며, 이 인수를 제외하고는 스레드를 사용하는 것과 큰 차이가 나지 않는다. 

파이버는 상호 컨텍스트 전환을 수동으로 해 주어야 하며 이 경우 주 스레드 역시 파이버로 변경되어야 컨텍스트 전환이 가능할 것이다. 따라서, 주 스레드를 파이버로 변경하는 ConvertThreadToFiber() 함수를 따로 제공하고 있다. 이 함수를 호출하게 되면 주 스레드가 파이버로 인식되며 (사실은 모든 파이버를 포함한 전체 주 스레드가 하나의 스레드로 시스템 상에서는 간주되는 것이지만) 파이버 상호간 컨텍스트를 수동으로 전환하는 SwitchToFiber() 함수를 사용하여 다른 파이버로 실행 권한을 넘기게 된다. 이 경우 제어권을 넘길 파이버는 어떤 파이버로 자신의 제어권을 넘길 것인지를 명시해 주어야 하며, 선언된 VOID형의 파이버 이름을 인수로 전달한다. 

위 프로그램을 컴파일하여 실행하면 지금까지 보았던 예와 동일한 결과를 얻을 수 있을 것이다. 그렇지만 파이버로 작성한 프로그램은 프로세스 내에서 스케쥴 퀀텀을 받을 수 있는 기회가 줄어든다는 것을 잊어서는 안 된다. 

8.2 작업 객체 

작업 객체는 프로세스와 스레드를 다루는 객체 단위 중 가장 큰 것으로, 일련의 프로세스를 하나의 작업(job)으로 묶어 놓은 것이다.

반응형
반응형

함수의 형태를 아래와 같이 생겼습니다.

DWORD WINAPI WaitForSingleObject(
  __in  HANDLE hHandle,
  __in  DWORD dwMilliseconds
);

위 함수는 커널 오브젝트의 상태 정보를 확인하는데 사용 됩니다.
즉, 해당 리소스의 커널 오브젝트가 Signaled인지 Non-Signaled인지 알 수 있습니다.

해당 리소스가 살아있으면 Non-Signaled (FALSE)이고 
해당 리소스가 종료되면 Signaled(TRUE)를 커널 오브젝트안에 가지고 있습니다.

첫번째 인자로는 해당 커널 오브젝트 핸들을 지정해 주고
두번째 인자는 Signaled 상태가 될 때까지 기다릴 수 있는 최대 시간을 밀리초 단위로 지정해 줍니다.
(INFINITE라고 인자를 주시면 해당 리소스가 종료 될 때까지 기다리고 있습니다. 
 즉, Signaled가 될 때까지 기다린다는 뜻입니다.)

자세한 내용은 MSDN을 참조해주세요 ^^

간단한 예제 소스로 결과를 확인해 봅시다.

===========================
예제 소스
===========================
#include <stdio.h>
#include <Windows.h>
#include <tchar.h>

int _tmain(int argc,TCHAR* args[])
{
STARTUPINFO start_info={0,};
PROCESS_INFORMATION process_info;

SetCurrentDirectory(_T("C:\\WINDOWS\\system32"));
TCHAR Command[] = _T("notepad.exe");

        /*메모장을 생성*/
CreateProcess(NULL,Command,NULL,NULL,TRUE,0,NULL,NULL,&start_info,&process_info);
       
       /*메모장이 종료 될때까지 멈춰있음*/
WaitForSingleObject(process_info.hProcess,INFINITE);


_tprintf(_T("Exit notepad. \n"));

return 0;
}

아마 실행해 보시면 메모장이 띄워지고 WaitForSingleObject 함수에서 멈춰 있는 것을 알 수 있습니다.
그리고 메모장을 끄시면 "Exit notepad."라는 문장이 출력되면서 예제소스 프로그램이 종료되는 것을 보실 수 있습니다.

======================================
쓰레드 동기화 시 사용되는 경우
======================================
쓰레드 동기화 시키는 방법에는 유저모드와 커널모드로 나눠지지만 WaitForSingleObject함수를 사용하는 경우는 
커널모드 쓰레드 동기화일 때만 사용합니다. 그에대한 2가지 방법으로  뮤텍스, 세마포어가 있습니다.

뮤텍스와 세마포어는 커널모드 쓰레드 동기화 방법 이므로 생성하면 커널 오브젝트 상태를 지닙니다.

1. 뮤텍스
임계영역에 들어가기 위해서는 무작정 들어가는 것이 아니라 키를 가지고 있어야만 들어갈 수 있는 것 입니다.
그럴 때 키는 얻는 함수로 사용되는 것이 WaitForSingleObject가 될 수 있습니다.
임계영역을 빠져나오면 키를 반환해야 하므로 그때 사용하는 함수는 ReleaseMutex가 되겠습니다.
간단하게 뮤텍스를 이용해 쓰레드들이 임계영역에 동시접근 하는 것을 막아보겠습니다.

상황을 간단하게 말하면
하나의 ATM기계가 있는데 10명의 사람들이 일을 보고 나오려고 하는 상황입니다.
각 사람들이 일보는 시간은 10~20초 사이입니다. ( 잘 안보이시면 클릭 )

위에 Working이라는 함수에서 WaitForSingleObject라는 함수를 사용하고 있는데 들어갈 수 있는 권한을 얻는 것입니다.
뮤텍스 커널 오브젝트도 마찬가지로 Non-Signed상태와 Signed상태가 있는데 해당 뮤텍스 커널 오브젝트가 Signed상태가 될때까지 기다리는 것입니다. Signed상태가 되면 기다리는 것들 중지하고 해당 쓰레드가 임계영역으로 들어가는 것입니다.

실행 결과 입니다.

동기화 방법 중에 뮤텍스 말고도 세마포어라는 것이 있는데 세마포어는 뮤텍스에 없는 카운트 기능이 있습니다.
즉, 임계영역에 동시에 들어 갈 수 있는 쓰레드의 개수를 정할 수 있는 기능이 있습니다.
하지만 WaitForSingleObject를 사용하는 방법은 같으므로 생략하도록 하겠습니다.

반응형
반응형

http://blog.naver.com/kki2406/80041209326

_beginthreadex와 _endthreadex 함수는 API 함수가 아니라 C Library 함수이다.

 

이 함수들에 관한 사용법을 알아보자...

 

API 함수와 매우 유사하다.

 

#include <windows.h>
#include <stdio.h>
#include <process.h>
#include <tchar.h>

 

unsigned counter;

 

unsigned __stdcall SecondThreadFunc( void *pArg )
{
 _tprintf(_T(" * In second thread... \n") );
 while ( counter < 1000000 ) counter ++;
 _endthreadex(0);
 return 0;
}

 

int main()
{
 HANDLE hThread;
 unsigned threadID;

 

 _tprintf( _T(" * Creating second thread... \n ") );
 hThread = (HANDLE) _beginthreadex(NULL, 0, SecondThreadFunc, NULL, 0, &threadID );
 
 WaitForSingleObject( hThread, INFINITE );

 _tprintf( _T(" * Counter should be 1000000; it is -> %d\n"), counter );

 CloseHandle( hThread );

 

 return 0;
}

 

 

SecondThreadFunc의 원형은 API의 CreateThread로 부르는 함수(Createthread 함수의 세번째 인자)의 원형과 동일하다.

 

 

이 _beginthreadex 함수는 내부적으로 CreateThread 함수를 호출하고 있다.

 

이를 보고 싶다면 MS사에서 제공하는 코드를 직접 보면 될 것이다.

 

이 코드 파일은 첨부토록 하겠다. _beginthreadex 함수를 찾아보면 알 것이다.

 

또한, thread 생성과 파괴시 다음과 같은 함수들이 존재한다.

 

학습 : CreateThread), ExitThread()
C Library + API : _beginthreadex(), _endthreadex();
MFC : AfxBeginThread(), AfxEndThread();

 

 

 


 

http://blog.naver.com/limsk112/70107328764

 

 

CreateThread 함수는 Win32 API 에서 제공되는 함수고

_beginthread,  _beginthreadex 함수는 C / C++ Runtime-Library 에서 제공되는 함수다.

 

_beginthread 함수는  이 함수로 새로운 쓰레드를 생성하고 난 후 바로 ::CloseHandle( ) 함수를 호출해서 생성한 쓰레드의 핸들을 닫아 버려서 생성한 쓰레드 오브젝트와의 통신을 할 수가 없다. 핸들을 바로 닫아버리는 이유는 Win32의 상세함을 숨기기 위함인데 결국 버그가 되어 버린 함수이다.

 

 이를 고안해서 새로 탄생한 함수가 바로 _beginthreadex 함수이다.

_beginthreadex 는 내부적으로 새로 생성한 쓰레드의 핸들을 닫지 않기 때문에 명시적으로 ::CloseHandle( ) 함수를 호출하여 쓰레드의 핸들을 수동으로 닫아 주어야 한다.

_beginthreadex 의 리턴값은 새로 생성된 쓰레드의 핸들값이다. 하지만 자료형이 정확히 일치하지 않기 때문에 적절하게 형변환을 해줘야 한다.

 

 

_beginthread 함수와 짝을 이루는 것이 _endthread 함수인데, 생성한 쓰레드가 종료되면, 이 함수가 자동 호출 되어진다. 따라서 이 함수는 명시하지 않으며, 명시를 해주면 에러가 난다.

 

_beginthreadex 함수와 짝을 이루는 것은 _endthreadex 함수다. 이 함수는 생성한 쓰레드가 종료되면, 역시 이 함수가 자동 호출되어 진다. 따라서 이 함수는 명시하지 않아도 되지만, 명시를 해도 _endthread 함수처럼 에러가 나는건 아니다.

 


[더 자세한 설명]- 아래까지 보는게 좋다

 

 

http://blog.naver.com/drags99/150032023121

 

두 함수는 모두 스래드를 생성할 때 호출하는 함수이다.

 

이 두 함수는 겉으로 보기엔 동일한 기능을 하지만 차이점이 있어서 이를 알고 넘어가야 한다.

MSDN을 보면 _beginthread 와 _beginthreadex 를 사용하면 스래드에 여러 인수를 전달할 수 있습니다라고 되어있다이러한 이유로 CreateThread 보다는 위 두 함수를 많이 이용한다.

위 두 함수 중에도 개인적으로는 _beginthreadex 함수를 주로 이용하고 있다.

위 두 함수의 차이점을 비교해 보면

함수 호출 규약이 _beginthread 함수는 __cdecl 이고, _beginthreadex 함수는 __stdcall 이다.

함수 호출 규약에 따라 내부적으로 인자 값들을 스택에 저장할 때 순서가 달라진다이러한 부분은 코딩을 하는 부분에서는 크게 신경 쓰지 않아도 되는 부분이다.

또 다른 차이점은 각각의 함수는 _endthread, __endthreadex 라는 함수와 같이 이용된다는 점이다딱 봐도 뒤에 ex 가 붙은 함수와 안 붙은 함수가 세트로 사용된다는 걸 쉽게 이해할 수 있을 것이다.

또 다른 차이점은 return Value 값이다.

_beginthread 함수의 경우 성공일 때 0, 실패일 때 -1을 리턴 하지만, _beginthreadex 함수는 실패일 때 0(NULL), 성공일 때 -1이 아닌 값을 리턴 한다.

_beginthread 함수는 성공과 실패를 구분하지만 _beginthreadex 함수는 실제 HANDLE 을 리턴하기 때문에 GetExitCodeThread 함수 호출 등이 가능하게 된다.

두 함수를 호출하는데 있어서 인자 값도 차이가 난다.

_beginthreadex 함수에서 initflag 값을 이용해 스래드가 생성되는 시점에서 스래드의 상태를 설정할 수 있다.

threadaddr 은 32bit 포인터 값으로 스래드의 주소 값을 반환 받을 수 있다.

Security 인자를 이용해 자식 프로세스에서 이 함수에서 반환된 스래드의 핸들을 상속받아서 사용할 수 있는지 유무를 결정할 수 있다.

위와 같은 차이가 나지만 대부분은 _beginthreadex 함수를 많이 이용할 것이다.

하지만 _beginthreadex 함수를 이용해야 하는 아주 결정적인 이유가 하나 더 있다.

Ansi 표준 라이브러리 함수를 호출할 경우 _beginthread 함수를 이용해 생성된 스래드 에서는 동기화 이유 때문에 안정성을 보장받지 못하게 되지만 _beginthreadex 함수를 이용할 경우 스래드별로 내부적으로 별도의 메모리 공간을 할당하기 때문에 동기화와 관련돼 안정성을 보장받을 수 있게 된다이렇게 스래드 생성시 할당된 별도의 메모리 공간을 해제하기 위해서 _endthreadex 함수와 짝을 이뤄서 이용하게 되는 것이다.

컴파일러에 따라 다르겠지만 .Net 에서는 멀티스래드에 안정적인 라이브러리를 제공하고 있기 때문에 별도의 선언이나 조작 없이도 안정적인 라이브러리를 이용할 수 있다고 한다.(아직까지는 별 문제 없이 잘 쓰고 있다.)

만약 _beginthreadex 함수로 스래드를 생성하고 _endthread 함수를 이용해 스래드를 종료하게 된다면 스래드 생성시에 할당된 메모리가 반환되지 않기 때문에 이렇게 스래드를 이용할 수 없다.

하지만 스래드를 생성할 때 인자로 넘긴 함수 포인터에서 return 을 이용해 스래드를 종료하게 되면 별도의 _endthreadex 함수를 호출하지 않아도 생성시에 할당된 메모리가 반환되기 때문에 return 을 이용하면 이 부분은 별로 걱정할 필요 없다.

스래드 종료 시 반환된 값은 내부에 생성된 스래드 커널에 저장되고, GetExitCodeThread 함수를 이용해 받아올 수 있다.

하지만 위 함수를 호출하기 전에 CloseHandle 함수를 이용해 스래드 핸들을 종료시킨다면 내부에 생성된 스래드 커널이 삭제되기 때문에 반환 값을 받을 수 없게 된다.

스래드의 반환 값을 이용할 필요가 있을 경우 GetExitCodeThread 함수를 이용해서 반환 값을 받아온 후에 CloseHandle 함수를 호출해 줘야 한다.

추가로 한가지 더 얘기 하면 CloseHandle 을 이용해서 함수 호출 시 반환된 스래드의 핸들을 종료시키게 되면 내부적으로 스래드 커널에 UC가 줄어들게 되 스래드가 종료되는 시점에서 스래드 커널도 바로 종료될 수 있다이 부분은 커널과 핸들러에 관한 내용을 미리 알고 있어야 해서 간단히 설명한다.

.. 두 함수를 비교하다 보니 결론적으로 코딩할때는 _beginthreadex 함수만 이용하게 된다.

솔직히 _endthreadex 함수도 거의 사용하지 않는다그냥 함수 내에서 return 을 이용해 스래드를 종료하고 있다.

 

반응형
반응형

http://lino.egloos.com/2403029

 

 

 

ExitThread에 대한 발견

흔히들 쓰레드 생성시에, CreateThread로 생성해서, ExitThread로 쓰레드 종료를 시키는 경우가 있는데, C에서라면, 리소스 정리를 프로그래머가 다 하기 때문에, ExitThread로 쓰레드를 종료시켜도 상관없지만, C++에서는 ExitThread로 쓰레드를 생성시키면 문제가 생길 수 있다.

예를 들어서 아래와 같은 클래스가 있을 경우..

class CFoo
{
  public:
   CFoo(){}
   virtual ~CFoo()
   {  
      OutputDebugString("CFoo Destructor is called! ");
   }
   void DoSomething() { OutputDebugString("Do Something! "); }
};

와 같은 코드가 있고, 쓰레드 프로시져로 아래의 함수가 있는 경우..

DWORD ThreadProc( LPVOID pParam)
{
   CFoo foo;
   ....
   foo.DoSomething();
   ....
   ExitThread(0);
}

이 경우, 소멸자가 불릴까? 안불릴까? 정답은 안불린다.
ExitThread가 쓰레드 스택을 말끔히 정리하면서, 소멸자나 리소스 해제같은거 안하고, 그냥 쓰레드를 종료시켜 버린다.

만약에 클래스에서 시스템 자원을 사용하고 있고, 소멸자에서 그 시스템 자원을 해제하는 경우, 바로 리소스 릭이 생긴다.

따라서, C++에서는 쓰레드 종료시 반드시, 종료 값을 리턴해 주는 방식으로 쓰레드를 종료시켜 줘야 한다.
_beginthread,  _beginthreadex 역시 마찬가지다.

그리고, _beginthread는 쓰레드 핸들을 자기가 닫는다, _beginthreadex와 ExitThread는 프로그래머가 직접 닫아 줘야 한다.

반응형
반응형

함수는 스택에 쌓인다

 

함수를 호출할때 쌓이는 것은

 

1. 함수 호출 인자

2. 리턴할 주소

3. 함수내의 지역변수

 

함수가 종료될때 시스템 스택에서 위 3가지가 제거된다

 

 

그런데 재귀호출이무한히 반복되면 스택에서 제거되지 않고 계속 쌓이다 보니 오버플로우가 발생 된다

반응형
반응형

메모리 상태 :


페이지의 개수 = 가상 메모리의 크기 / 페이지 하나당 크기

페이지 개수는 가상 메모리의 크기에 비례하며(가상 메모리는 몇 비트 환경인지에 비례 (ex. 32비트 4GB)),

모든 페이지는 Reserve, Commit, Free 세가지 중 하나의 상태를 지닌다.

Commit : 물리 메모리에 할당된 상태

Reserve : Free 와 Commit 의 중간상태이다. 해당 번지에 대해 예약을 한다.
              다른 메모리 함수가 물리 메모리에 해당 번지에 할당하지 못하도록 한다.
              하지만 물리 메모리의 소비는 발생하지 않는다.
                
Free : 물리 메모리 할당이 이뤄지지 않은 상태


메모리 할당의 시작점과 단위 확인 :

가상 메모리 시스템은 페이지 단위로 관리된다.

페이지의 중간 위치에서부터 할당을 시작할수 없으며, 페이지 크기의 배수 단위로 할당한다.

Allocation Granularity Boundary : 메모리 할당의 시작 주소가 되는 기본 단위
(참고 : 메모리가 지나치게 조각나는것과 관리의 효율성을 이유로 페이지 크기의 배수보다 더 넓은 값을 가진다)

Allocation Granularity Boundary 과 페이지 크기 확인 :

#include <stdio.h>
#include <tchar.h>
#include <windows.h>

int _tmain(int argc, TCHAR *argv[])
{
          SYSTEM_INFO si;
          DWORD allocGranularity;
          DWORD pageSize;
 
          GetSystemInfo(&si);
          pageSize = si.dwPageSize;
          allocGranularity = si.dwAllocationGranularity;

          _tprintf(_T("Page Size : %u Kbyte \n"), pageSize/1024);
          _tprintf(_T("Allocation granularity : %u Kbyte \n"), allocGranularity/1024);
 
          return 0;
}



가상 메모리 컨트롤 :

페이지 상태를 RESERVE 나 COMMIT 로 변경할때 사용하는 함수

LPVOID VirtualAlloc(
          LPVOID lpAddress,       // 예약 및 할당하고자 하는 메모리의 시작 주소
                                            // (일반적 NULL 전달, RESERVED -> COMMIT 일때 해당페이지 시작 주소 지정)
          SIZE_T dwSize,                // 할당하고자 하는 메모리의 크기를 바이트 단위로 지정
                                                 // 메모리의 할당은 페이지 크기 단위로 결정
          DWORD flAllocationType,    // 메모리 할당의 타입.
                                                 // RESERVE : MEM_MRESERVE,          COMMIT : MEM_COMMIT 
          DWORD flProtect                // 페이지별 접근방식에 제한을 두는 용도
                                                 // RESERVE 상태로 변경할때는 접근을 허용하지 않는 PAGE_NOACCESS ,
                                                 // COMMIT 상태로 변경할때는 읽고/쓰기 모드인 PAGE_READWRITE 전달
);

함수 호출이 성공하면 할당이 이뤄진 메모리의 시작 주소를 리턴


할당된 페이지를 되돌리는 함수

BOOL VirtualFree(
           LPVOID lpAddress,            // 해제할 메모리 공간의 시작 주소 
           SIZE_T dwSize,                // 해제할 메모리 크기를 바이트 단위로 지정
           DWORD dwFreeType         // MEM_DECOMMIT : 해당 페이지의 상태를 RESERVE 상태로 되돌린다.
                                                  // MEM_RELEASE : 해당 페이지의 상태를 FREE 상태로 되돌린다.
                                                  //                           두번째 전달인자 dwSize 는 반드시 0 이어야 한다.
);



힙 컨트롤 :

디폴트 힙(Default Heap) : 

디폴트 힙을 구성하는 페이지들은 RESERVE 상태이다. 일부는 성능을 위해 COMMIT 상태일수도 있다.

C 언어의 malloc 함수와 free 함수, C++ 언어의 new와 delete 를 사용해서 힙영역을 사용할 경우

프로세스를 생성할때 생성되는 힙, 즉 1M 바이트 크기의 디폴트 힙에 메모리를 할당한다.
(COMMIT 와 RESERVE 상태를 오간다)

프로세스에 기본적으로 할당되는 힙이며 프로세스 힙(Process Heap)라고도 한다.


디폴트 힙 컨트롤 :

디폴트 힙의 기본 크기는 1MB 이다. 링커(Linker)옵션을 통해 크기를 변경할 수 있다.

/HEAP : reserve, commit  (ex. /HEAP 0x200000, 0x10000  :  디폴트 힙의 크기 : 2MB,       COMMIT 크기 : 64KB)

힙은 동적이라 기본 크기 1MB 로 생성이 된 후 필요에 따라 그 크기가 Windows 시스템에 의해 자동으로 늘어난다.



Windows 시스템 힙(Dynamic Heap) : 

Windows 시스템 함수를 통해 여러 힙을 추가로 생성할 수 있다.

가상 메모리 범위 내에서 프로세스 생성시 만들어지는 디폴트 힙 이외에 필요로 하는 힙을 얼마든지 생성할 수 있다.


힙(Dynamic Heap) 생성이 가져다 주는 이점 :

1. 메모리 단편화의 최소화에 따른 성능 향상 :



2. 동기화 문제에서 자유로워짐으로 인한 성능 향상 :

힙은 쓰레드가 공유하는 메모리 영역이다. 따라서 둘 이상의 쓰레드가 동시접근 할때 문제가 발생할수 있으므로

Windows 내부적으로 동기화처리를 해준다.(여기서 동기화는 메모리 할당과 해제)

같은 주소 번지에 둘 이상의 쓰레드가 동시에 메모리를 할당 및 해제하는 상황이 발생할 경우 메모리 오류(Corrupt)가 
발생하므로 디폴트 프로세스 힙은 동기화 처리를 하는데 쓰레드마다 독립된 힙을 가지고 있다면

이러한 동기화가 필요없으므로 성능이 향상된다.


힙(Dynamic Heap) 컨트롤 :

힙을 생성하는 함수 :

HANDLE HeapCreate(
         DWORD flOptions,                 // 생성되는 힙의 특성을 부여 (0 을 전달시 가장 일반적인 힙 생성)
                                                   // HEAP_GENERATE_EXCEPTIONS : 오류 발생시 NULL이 아닌 예외 발생
                                                   // HEAP_NO_SERIALIZE : 생성된 힙의 메모리 할당과 해제에 대해
                                                   //                                   동기화 처리를 하지 않는다.
                                                   // 둘 이상의 속성을 비트 단위 연산자 OR( | )로 동시 지정 가능
         SIZE_T dwInitialSize,            // dwMaximumSize 에서 지정한 메모리 중에서
                                                   // 초기에 할당할 COMMIT 페이지를 지정한다 
         SIZE_T dwMaximumSize      // 생성되는 힙의 크기 결정.
                                                   // 지정하는 크기에 해당하는 페이지의 수만큼 RESERVE 상태가 된다.
                                                   // 0이 아닐 경우 힙은 증가가능한 메모리(Growable Heap)가 된다
);

힙을 소멸하는 함수 :

BOOL HeapDestroy(
         HANDLE hHeap                    // 반환하고자 하는 힙의 핸들
);  

힙에 메모리를 할당하는 함수 :

LPVOID HeapAlloc(
         HANDLE hHeap,               // 메모리 할당이 이뤄질 힙의 핸들
         DWORD dwFlags,             // HEAP_GENERATE_EXCEPTIONS : 오류 발생시 NULL이 아닌 예외 발생
                                               // HEAP_NO_SERIALIZE : 함수호출시 동기화 처리되지 않는다.
                                               // (HeapCreate 함수호출에서 지정했다면 중복지정할 필요는 없다)
                                               // HEAP_ZERO_MEMORY : 할당된 메모리는 0으로 초기화
                                               // 둘 이상의 속성을 비트 단위 연산자 OR( | )로 동시 지정 가능
         SIZE_T dwBytes               // 할당하고자 하는 메모리의 크기를 지정
                                               // (증가가능한 힙이 아닐 경우 최대 크기 0x7FFF8)
); 

힙에 메모리를 해제하는 함수 :

BOOL HeapFree(
         HANDLE hHeap,                // 해제할 메모리를 담고 있는 힙을 지정
         DWORD dwFlags,              // HEAP_NO_SERIALIZE 가 인자로 올 수 있다,     일반적 : 0
                                                // (HeapCreate 함수호출에서 지정했다면 중복지정할 필요는 없다)
         LPVOID lpMem                 // 해제할 메모리의 시작 주소 지정
);

반응형
반응형

반응형
반응형



http://ezbeat.tistory.com/206

 

1. 종료 핸들러

종료 핸들러는 __try와 __finally로 이루어져 있습니다.

__try{
        Ezbeat 루틴
}
__finally{
        Hacker 루틴
}
코드를 보시면 __try안으로 들어와서 Ezbeat루틴을 실행하게 되면 __try루틴을 빠져나올 때 항상 __finally루틴을 실행하게 됩니다. 위 코드는 너무 추상적 이므로 직접 소스를 짜서 보도록 하겠습니다.

두 수를 입력 받아서 나누는 프로그램 입니다.

위 소스코드에서는 2가지 실험 결과를 통해 이해를 하실 수 있습니다. 결과를 보시겠습니다.


첫번째 결과에서는 9와 3을 나눠서 Result가 3이었습니다. 
__try를 빠져나오면서 __finally에 있는 코드를 실행 한 것을 볼 수 있습니다.

두번째 결과를 보시면 5와 5를 나눠서 Result가 1이었습니다.
그러면 __try에 있는 if문안으로 들어오게 됩니다. 리턴을 하게 되면서 main함수가 종료가 되겠지요.
하지만 __finally에 있는 코드는 여전히 실행을 하게 됩니다.

위 소스코드와 결과만 봐도 이해를 하셨을 것입니다. 그러면 __try루틴에 들어오게 되면 반드시 __finally루틴이 실행되느냐..
그건 아닙니다. 모든과정에 예외가 있듯이 이것도 예외가 있습니다.

몇몇 종료 함수들을 사용하게 되면 __finally루틴으로 들어오지 않고 프로그램이 끝나게 됩니다.
( ExitProcess, ExitThread, exit .. )
소스코드와 결과를 보시겠습니다.

return부분을 ExitProcess함수로 바꾸었습니다.

결과를 보면 __finally루틴으로 들어오지 않고 종료가 된 것을 볼 수 있습니다.

결론입니다.
__try 루틴을 빠져나오는 경우 ( __finally 루틴 실행 함 )   : return, continue, break, goto, 예외발생
__try 루틴을 빠져나오는 경우 ( __finally 루틴 실행 안함) : ExitProcess, ExitThread, TerminateProcess, exit

상황에 맞게 적절히 종료 핸들러를 사용해 더욱 깔끔한 소스코드를 짜보세요 ^^

2. 예외 핸들러
예외 핸들러는 __try와 __except로 이루어져 있습니다.
__try{
      예외발생 루틴
}
__except(예외처리 방식){
      예외가 발생됬을 때 처리하는 루틴
}
__try 안에서 예외가 발생하면 예외처리 방식에 따라 해당 예외를 처리하게 됩니다.

예외처리 방식에 올 수 있는 경우의 수는 3가지 입니다.
EXCEPTION_EXECUTE_HANDLER : 예외가 발생하면 __try루틴의 나머지 부분을 실행하지 않고 예외처리 루틴으로 감
EXCEPTION_CONTINUE_EXECUTION : 예외가 발생한 지점으로 돌아가 다시 실행하도록 하는 방식
EXCEPTION_CONTINUE_SEARCH : 함수가 호출된 순서를 바탕으로 예외핸들러를 다시 찾아서 예외처리 해라

보통 많이 쓰이는 것은 위 2가지 이고 마지막은 잘 쓰지 않습니다.
먼저 각각의 경우를 소스코드를 짜서 보여드리도록 하겠습니다.
EXCEPTION_CONTINUE_SEARCH 경우는 잘 쓰지 않으므로 설명은 따로 하지 않겠습니다.

ㄱ. EXCEPTION_EXECUTE_HANDLER

위 소스코드와 크게 다른 것은 없습니다. try에서 예외를 발생시켰을 때와 안시켰을 때의 결과를 보도록 하겠습니다.

예외발생 안시켰을 때


예외발생 시켰을 때


예외를 발생시키면 __try에서 남은 코드를 실행 시키지 않고 바로 예외처리 루틴으로 가게 되는 것을 볼 수 있습니다.

ㄴ. EXCEPTION_CONTINUE_EXECUTION

소스코드가 약간 복잡합니다.

출력 결과를 본 후에 설명을 하도록 하겠습니다.

처음에 5와 0을 입력 했더니 예외가 발생해서 Func함수로 들어왔습니다. (GetExceptionCode는 뒤에서 설명하겠습니다.)
해당 예외가 정수를 0으로 나눴을 때 발생한 예외이므로 if문 안으로 들어오게 됩니다.
안에서 입력받는 수가 두개의 정수를 다시 입력 받는게 아니라 뒤에 들어온 수만 입력을 받고 있습니다.

여기서 궁금하신 분이 계실 것입니다. 두 수를 전부다 입력 받아도 되겠지..라고 생각하신 분들이 계실 것입니다.
제가 테스트를 해보았습니다.

Func함수 내부를 살짝 바꾸었습니다. 두 수를 입력 받도록 되어 있습니다. 출력 결과를 보도록 하겠습니다.

마지막 결과를 따르면 Result가 3이 나와야 할 탠데 결과는 5가 나왔습니다.
첫번째 값은 계속 넣어도 무용지물인지에 대해 알아본 결과

예외가 발생한 이후에 나눗셈을 할 때 첫번째 수는 처음 입력한 수를 특정 메모리에 넣어두고 
정상적인 나눗셈을 수행할 때 처음 입력했던 값을 EAX레지스터로 옮긴다음에 두번째 수와 나눗셈을 수행하게 됩니다.
( 메모리에 넣어둔 값을 EAX로 옮기는 과정은 커널영역에서 하는 것 같습니다. )

이 부분은 이해하기가 어려우므로 그냥 그런다고 넘어가시면 됩니다.
쫌더 쉽게 말해보면 두 수를 나눌 때 처음 입력 받은 수는 특정 메모리에 저장되어 있다가 
다시 나눗셈을 수행할 때 가져와서 두번째 수와 나눗셈을 수행한다는 것입니다.
아래그림을 보시면 어느정도 이해가 되실 것입니다.


이정도면 이해가 되셨을꺼 같으니 결론을 내보면 EXCEPTION_CONTINUE_EXECUTION 옵션은 예외가 발생한 지점으로 다시 가서 코드를 실행하라는 뜻이됩니다.

위에서 설명하겠다고 한 GetExceptionCode입니다.
GetExceptionCode은 함수가 아닌 메크로 입니다. 예외가 발생했을 때 어떠한 예외가 발생했는지 알려주는 메크로 입니다.
많은 경우의 수가 있는데
위 링크로 가시면 볼 수 있습니다.

이러한 예외처리 방법을 코드상에서 많이 볼 수 있는데 알고있으면 좋을것 같아서 한번 써보았습니다.
그리고 이러한 예외처리 방법을 사용한 안티리버싱 기법도 있습니다.


반응형
반응형


  1. Structured Exception Handling
  2. Termination Handling
  3. Exception Handling
  4. Stack Unwind
    1. Local Unwind
    2. Global Unwind
  5. Unhandled Exception
  6. First and Second Chance Exception
  7. 링크



1 Structured Exception Handling

  • 윈도우즈에서 제공하는 예외 처리 방식이다. (사실 실제 구현은 컴파일러에서 이루어진다.) 
  • C++ 예외와는 다른 것이다. 
  • 윈도우즈 상에서는 C++ 스탠다드 라이브러리도 내부적으로 SEH를 이용한다. 
  • C++ 예외 처리는 당연히 C++에서만 사용할 수 있으나, SEH는 언어 중립적이기 때문에 다른 언어에서도 사용 가능하다. 
  • Termination Handling과 Exception Handling으로 분류할 수 있다. 


2 Termination Handling

    __try { // Guarded code } __finally { // Termination handler }
    finally 블록 안의 안의 코드는 try 블록 안에서 무슨 짓을 하든 반드시 실행된다. try 블록 안에서 return을 해도, goto를 해도, longjump 명령을 직접 호출해도, 결국은 실행된다. 좀 더 자세하게 말하자면, return이나 goto를 하기 직전에 finally 블록 안의 코드가 실행된다. (exit, abort, ExitProcessTerminateThread 등을 통해 프로세스나 스레드가 종료되는 경우는 예외다. 이런 경우에는 finally 블록의 코드가 실행되지 않는다.) 

    try 블록 안에서 return, goto, longjump 등을 사용하는 것은 자제하는 것이 좋다. 반환값 문제 때문에 컴파일러가 추가적인 코드를 만들어내기 때문이다. 이 코드의 양은 CPU마다 틀려지는데, 수백에서 수천 사이클까지 걸리는 경우가 있다. 그러므로 자주 사용하는 코드에다가 집어넣어 놓으면 프로그램의 성능이 심각한 수준까지 떨어질 염려가 있다. 

    코드의 흐름이 자연스레 finally 블록까지 흘러가는 경우(return 등을 사용하지 않는 경우), 오버헤드는 거의 없다. x86 계열의 CPU에서 마이크로소프트 컴파일러를 사용한 경우, 단 하나의 명령어를 실행할 뿐이다. 

    일반적으로 가장 좋은 방법은 try 블록 안에서 return, continue, break, goto 문 등을 쓰지 않는 것이다. 이들은 try 블록 바깥 쪽으로 빼내줘야 한다. 그래도 어쩔 수 없이 try 블록을 빠져나가고자 한다면 __leave 키워드를 사용하면 된다. 
    bool some_function(const char* param) {
    	 bool bOK = false;
    	 char* pBuffer = NULL; ... 
    	__try { 
    		pBuffer = new char[100];
    		 if (pBuffer == NULL) { 
    			// return false; 
    			// 여기서 false를 리턴할 것이 아니라, __leave를 사용한다.
    			__leave; 
    		}
    		 bOK = true; 
    	} 
    	__finally { 
    		if (pBuffer) 
    			delete [] pBuffer; 
    	} 
    	return bOK; }
    __leave 키워드를 사용하면, 코드의 흐름(instruction pointer)이 try 블록이 끝나는 곳(__finally 바로 앞의 괄호)으로 이동한다. 그 다음 finally 블록 안의 코드가 자연스레 실행된다. 위에서도 언급했듯이 자연스러운 이동은 성능에 영향을 거의 주지 않는다. 


3 Exception Handling

    __try { // Guarded code }
     __except (EXCEPTION_EXECUTE_HANDLER) // Exception filter 
    { // Exception handling code }
    except 부분은 C++ 예외 처리 구문의 catch와 비슷해 보이지만, 다르다. C++ 예외 처리에서는 이 부분에 예외의 종류 밖에 들어갈 수 없지만, SEH에서는 exception filter라고 불리는 C 구문(expression)이 들어간다. exception filter 구문은 계산을 거친 후, 최종적으로 세 가지 값 중의 하나가 되어야 한다. 

    • EXCEPTION_CONTINUE_EXECUTION (-1) 
        예외를 무시하고, 예외가 발생한 지점에서부터 프로그램을 계속 실행한다. 예를 들어 10 / i 에서 i가 0이라서 예외가 발생한 경우, 예외 처리 필터가 이 값이라면, 다시 10 / i부터 실행한다는 말이다. 
    • EXCEPTION_CONTINUE_SEARCH (0) 
        except 블록 안의 코드를 실행하지 않고, 상위에 있는 예외 처리 핸들러에게 예외 처리를 넘긴다. 
    • EXCEPTION_EXECUTE_HANDLER (1) 
        except 블록 안의 코드를 실행한 후, except 블록 아래의 코드를 실행한다. 

    이 세가지 값을 이용해서 예외 처리 코드를 샘플로 만들어 보자면 다음과 같다. 

    • try 블록 안에서 예외가 발생한 경우, 그 예외가 ACCESS_VIOLATION일 경우에는, exception 블록 안의 코드를 실행하고, 다른 예외인 경우에는 상위에 있는 예외 처리 핸들러에게 통제를 넘기는 코드. (GetExceptionCode 함수는 try 블록 안에서 발생한 예외의 종류를 반환하는 함수다. 자세한 것은 MSDN을 참고하도록.) 
      __try { ... // compute something } 
    • __except (GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ?
    •   EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
    •  { cerr << "Access violation happened!" << endl; }
      
      
      
      
    • try 블록 안에서 발생한 예외에 대한 정보를 검사하는 필터 함수를 만든 후, 이를 exception filter 부분에서 실행해서 어떤 행동을 해야할 지를 판단하는 코드. (GetExceptionInformation 함수는 예외와 예외가 발생한 시점에서의 시스템에 대한 정보를 반환하는 함수다. 자세한 것은 MSDN을 참고하도록.) 
      DWORD decide_what_to_do(PEXCEPTION_POINTERS pExceptions)
    •  { // check exceptions ...
    • 	 return EXCEPTION_CONTINUE_EXECUTION;
    •  }
    •  __try 
    • { ... // compute something } 
    • __except(decide_what_to_do(GetExceptionInformation())) 
    • { ... }
      Exception Handling의 경우, Termination Handling과는 달리, try 블록 안에 return, goto 등이 있어도 코드 크기나 성능에 페널티가 없다. 


4 Stack Unwind

    4.1 Local Unwind

      try-finally 구문에서 try 블록 내부에 return, goto 등의 premature exit가 있을 경우 발생한다. finally 블록 실행 후, 값을 리턴해야하기 때문에, 컴파일러는 임시 변수를 생성해서, 리턴값을 저장한 후, finally 블록을 실행하고, 저장해둔 값을 리턴한다. 위에서도 나와있듯이 이런 코드가 꽤 비용이 크기 때문에 극구 피해야한다. 

    4.2 Global Unwind

      Global unwind는 try-except 구문에서 exception filter가 EXCEPTION_EXECUTE_HANDLER 값인 경우 발생한다. 동작을 요약하자면, EXCEPTION_EXECUTE_HANDLER로 판정된 try 블록 안의 모든 finally 블록을 제일 안쪽의 것부터 실행한 다음, 원래 try 블록의 except 블록 부분을 실행한다가 되겠다. 

      다음은 Programming Applications from Microsoft Windows에서 가져온 예제이다. 짐작하듯이 주석에 있는 번호는 실행 순서를 나타낸다. 
      void function1() 
      { 
       // 1. 여기서 뭔가를 처리 ...
        __try
        { 
       // 2. 다른 함수를 호출 function2();
        // 이 부분의 코드는 실행되지 않는다. ... 
       }
        __except ( /* 6. 필터 구문을 검사 */  EXCEPTION_EXECUTE_HANDLER)
        { // 8. Unwind가 일어난 후에, 예외 처리 핸들러가 실행된다.
        MessageBox(...); 
       } 
      } 
       
      void function2() 
      { // 3. 여기서 뭔가를 처리 ...
        __try { // 4. 뭔가에다 락을 건다.
        EnterCriticalSection(&C); // 5. 여기서 예외가 발생한다.
        int some = 10 / 0; 
       } 
       __finally 
       { // 7. 상위 함수의 exception filter 부분의 값이 
       // EXCEPTION_EXECUTE_HANDLER이기 때문에 Global unwind가 일어난다. 
       // 락을 푼다.
        LeaveCriticalSection(&C);
        } // 이 부분의 코드는 실행되지 않는다. }
      왜 실제로 예외가 발생한 finally 부분이 먼저 실행되지 않느냐고 할 수 있는데, 예외가 발생한 경우, finally 보다는 except를 먼저 찾아서 처리한다고 생각하면 된다. 왜 그런지는 아직까지 잘 이해가 가지 않는다. 어쨌든 중요한 것은 finally 부분은 반드시 실행이 된다는 점이다. 


5 Unhandled Exception

    예외가 발생해서 그걸 처리하는 try-except 부분을 차례대로 검사했는데, 모두가 EXCEPTION_CONTINUE_SEARCH를 반환한 경우를 Unhandled Exception이라고 한다. 즉 해당하는 예외를 처리할 핸들러가 하나도 없다는 말이다. 

    윈도우즈 상에서 모든 스레드는 Kernal32.dll에 있는 BaseProcessStart와 BaseThreadStart 함수를 통해 실행된다. 첫번째는 프로세스의 메인 스레드를 위한 것이고, 두번째는 추가적인 스레드를 위한 것이지만, 결국 똑같은 넘이다. 
    VOID BaseProcessStart(PPROCESS_START_ROUTINE pfnStartAddr) 
    {
      __try { 
      ExitThread((pfnStartAddr)()); 
     }
      __except(UnhandledExceptionFilter(GetExceptionInformation()))
      { 
     ExitProcess(GetExceptionCode()); 
     } 
    } 
    VOID BaseThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam)
     { 
     __try {
      ExitThread((pfnStartAddr)(pvParam)); 
     }
      __except(UnhandledExceptionFilter(GetExceptionInformation())) {
      ExitProcess(GetExceptionCode());
      } 
    }
    결국 Unhandled Exception이 발생한 경우, 시스템에서 이를 잡아서 UnhandledExceptionFilter 함수를 실행한다는 것을 알 수 있다. 이 함수는 사용자들에게 친숙한 다음 대화창을 표시한다. 

    crash.gif 

    여기서 "확인"을 누르게 되면 EXCEPTION_EXECUTE_HANDLER를 반환하고, global unwind를 일으켜, 결국 ExitProcess를 호출하게 한다. 이것이 확인을 누르면 프로그램이 종료되는 이유다. "취소"를 누른 경우, 시스템은 JIT(Just In Time) 디버거를 로드하게 된다. 

    모든 스레드의 주 루프를 손수 만든 try-except (EXCEPTION_EXECUTE_HANDLER) 구문으로 감싸면, 예외가 위의 함수까지 가지 않게 된다. 이는 위의 대화창을 표시하지 않고, 다른 행동을 할 수 있다는 말이다. 그러나 모든 스레드를 이런 식으로 감싸는 것보다는 SetUnhandledExceptionFilter 함수를 이용하는 것이 낫다. 
    LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter( LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter ); 
    // TOP_LEVEL_EXCEPTION_FILTER 함수의 형식 
    LONG UnhandledExceptionFilter( STRUCT _EXCEPTION_POINTERS* ExceptionInfo );
    이 함수를 사용하는 예는 MiniDump 페이지를 참고하기 바란다. 


6 First and Second Chance Exception

    예외 핸들러를 설치한 경우, 예외가 발생하면 핸들러에서 그것을 잡아서 처리한 후, 실행을 계속한다는 개념은 예외 처리의 기본이다. 그런데 실행 중인 프로그램에 디버거가 붙어있는 경우에는 약간 다르다. 디버거가 붙어있는 상태에서의 예외 처리는 다음과 같은 순서로 이루어진다. 

    1. 디버거에서 첫번째로 예외가 발생한다. --> First Chance Exception 
    2. 프로그램 내부의 예외 핸들러에서 예외가 발생한다. 
    3. 디버거에서 두번째로 예외가 발생한다. --> Second Chance Exception 

    물론 첫번째로 디버거에서 예외가 발생했을 때, 프로그램 쪽으로 예외를 넘겨주지 않으면 거기서 끝이다. 또한 프로그램 내부에서 예외 핸들링을 제대로 하지 않은 경우에도 한번으로 끝이다. 이 경우는 원래 프로그램이 크래쉬되는 상황이라는 것은 두말할 나위 없다. 더 자세한 내용은 [WWW]INFO: First and Second Chance Exception Handling를 참고하시라. 


7 링크


출처 http://excel96.cafe24.com/moin.cgi/SEH

반응형
반응형

CreateMutex() 함수에 대해서 좀 제대로 설명 좀 해주세요. 그 이 전에 뮤텍스가 뭔지, 스레드, 프로세스, 프로그램간의 관계를 좀 잘 좀 설명 해주시고, CreateMutex함수에 대해서 좀 설명 좀 잘 좀 해주세요..

 

 


쓰레드(Thread)는 멀티프로세스가 지원되는 OS에서 실행의 최소 단위입니다..

쓰레드가 모여서 하나의 프로세스를 구성하지요... 그러니까 프로세스를 기동시키면

한개 이상의 쓰레드가 기동되는것을 의미하는것죠....

하나의 프로세스에 여러개의 쓰레드를 기동시키기도 합니다..

프로세스는 말 그데로.. 독립적인 실행의 단위입니다...

대충 프로세스와 쓰레드는 설명이 되었고..

Visual C++을 사용하면 동기화에 관련된 몇가지 모듈을 제공합니다..

크리티컬섹션(CriticalSection)

뮤텍스(Mutex)

세마포어(Semaphore)

그 밖에 기타 Event, Overlapped 등을 지원합니다...

동기화(상호배제)에 관해서는 님께서 대충 아신다고 하니.. 그 부분은 설명하지 않고..

크리티컬섹션과 뮤텍스의 차이점과 CreateMutex()에 관해서만 설명드리겠습니다..

크리티컬섹션이나 뮤텍스는 어떻게 보면 기능상 똑같다고 생각하시면 됩니다. 약간은 차이는 나지만..

근본적인 차이는 크리티컬섹션은 하나의 프로세스 안에서만 동작하는 것이고

(그러니까 하나의 프로세스안에 여러개의 쓰레드가 있을경우 그 쓰레드들간의 상호배제를 구현하는것이지요)

뮤텍스는 문자열로 뮤텍스의 이름을 지정해 주면 뮤텍스의 이름으로 프로세스간에도 상호배제를 사용할 수 있습니다..

윈도우즈의 뮤텍스와 관련된 함수는 다음과 것들이 있습니다.

HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpName);
HANDLE OpenMutex(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);
BOOL ReleaseMutex(HANDLE hMutex);

CreateMutex()함수는 뮤텍스를 생성합니다.
이미 생성된 이름을 갖는 뮤텍스의 핸들을 얻기 위해서는 OpenMutex()를 사용합니다.
뮤텍스의 사용이 끝나서 해당 뮤텍스를 놓아 줄때는 ReleaseMutex()함수를 사용합니다.
위에 원형은 밝히지 않았지만 생성한 뮤텍스를 파괴시킬때는 모든 커널객체가 그렇듯 CloseHandle()함수를 사용합니다.
참고로 신호상태인 뮤택스를 얻기 위해서는 위에서 설명한 대기함수를 사용해야 한다는 것은 짐작하셨겠죠?

이때 CreateMutex()는 이미 다른 프로세스에서 사용하고자 하는 뮤텍스를 생성하였다면 CreateMutex()함수를 호출하지

 않고 OpenMutex()를 사용해도 되지만 CreateMutex()함수를 다시 한번 더 호출해도 상관이 없습니다.

CreateMutex()함수의 아큐먼트는 다음과 같습니다.
- lpMutexAttributes : 뮤텍스의 보안 속성을 지정하는 것으로서 주로 상속관계를 지정하기 위해 사용됩니다. 일반적으로는

 NULL을 입력합니다.
- bInitialOwner : 뮤텍스를 생성하면서 사용권한을 갖을 것인지를 결정합니다. 즉, TRUE를 입력하면 생성하자 마자

 뮤텍스의 사용권한(비신호상태의 뮤텍스 생성)을 갖습니다. FALSE를 입력할 경우 뮤텍스만 생성하기만(신호상태의 뮤텍스 생성)

 하고 실제 사용권한을 얻을때는 대기함수를 사용해야 합니다.
- lpName : 뮤텍스에 이름을 문자열로 지어 줍니다. 이름이 지정된 뮤텍스를 만듦으로서 이 이름을 아는 다른 프로세스와의

 동기화를 할 수 있습니다. 하나의 프로세스에서만 동기화 방식으로 뮤텍스를 사용한다면 NULL을 입력하여 이름을 지어 주지

않을 수도 있습니다. 참고 이름을 지어준 뮤텍스를 명명된 뮤텍스(named mutex)라고 합니다.

CreateMutex()함수는 생성한 뮤텍스의 핸들을 리턴합니다. 만약 생성하려는 뮤텍스의 이름으로 이미 뮤텍스가 생성되어

 있는 경우는 해당 뮤텍스 핸들을 리턴하고 GetLastError()로는 ERROR_ALREADY_EXISTS값을 얻을 수 있습니다. 만약

생성되어 있는 뮤텍스에 접근할 수 있는 권한이 없는 경우(ERROR_ACCESS_DENIED) NULL을 리턴합니다. 구체적인

원인을 알기 위해서는 GetLastError()함수를 호출하여 알 수 있습니다. 만약 EROR_ACCESS_DENIED일 경우는

OpenMutex()함수를 사용하여야 합니다.

OpenMutex()함수의 아큐먼트는 다음과 같습니다.
- dwDesiredAccess : 접근속성
- bInheritHandle : 상속옵션
- lpName : 지정된 뮤텍스의 이름

OpenMutex()함수는 이름이 지정된 뮤텍스의 핸들의 핸들을 리턴합니다. 만약 지정된 이름의 뮤텍스가 없는 경우는

 NULL을 리턴하고 GetLastEror()함수는 ERROR_FILE_NOT_FOUND값을 리턴합니다.

일반적으로 OpenMutex()함수는 잘 사용하지 않습니다. 왜냐하면 CreateMutex()함수로 새로운 뮤텍스를 생성할 수도

 이미 생성된 뮤텍스의 핸들을 얻을 수도 있기 때문이죠.
위에서 이야기 한대로 뮤텍스에 보안 속성을 설정한 경우 OpenMutex()를 사용하기도 합니다.

ReleaseMutex()함수는 잡았던 뮤텍스(비신호상태의 뮤텍스)를 다시 놓아주는(신호상태로 만들어 주는) 함수입니다.
아큐먼트는 hMutex에 뮤텍스 핸들을 넣어 줍니다. 놓아주기에 성공하면 TRUE를 실패하면 FALSE를 리턴합니다.

뮤텍스 설명에 관한 출처...

http://blog.naver.com/herryjoa?Redirect=Log&logNo=110028833001

그리고 위에 설명 해 놓은것은 API이고..

MFC를 사용하신다면 CMutex Class를 사용하시면 편리합니다...

사용방법은 비슷하지만. CMutex Class가 훨씬 사용하기가 편합니다....

 

반응형
반응형

[네트워크C++ - 동기화]쓰레드 동기화(실행순서 동기화) -이벤트,타이머  쓰레드 / [네트워크기초]

2008/01/17 21:54

복사http://blog.naver.com/blue7red/100046389559

1.순서동기화의 이유

 

-생산자/소비자 모델

외부-->입력 -->쓰레드1(생산자쓰레드) -->버퍼(20메가)-->쓰레드2(소비자쓰레드) -->출력

 

출력은 입력에 의존적이다.

입력은 외부환경에 의존적이다.

 

출력하고 있는데 갑자기 엄청난 입력(10메가)이 들어온다면 데이터의 손실이 발생할 수 있기 때문에 위와 같이 구성한다.

 

버퍼가 비워있는 상태에서 생산자가 데이터를 날라주기전에 소비자가 버퍼에서 가져오면 쓰레기값이 가져오게된다.

 

2.이벤트 기반의 동기화

 

-생산자가 데이터를 가져다 놓았다는 것은 생산자만이 알 수 있다.(결정대상은 프로그래머가 결정한다)

-소비자는 WaitForSingleObject()로 대기중(블로킹상태)

-생산자가 데이터를 가져다 놓으면 시그널상태로 커널오브젝트를 바꾼다.(SetEvent)

-이제야 소비자는 데이터를 가져온다.

 

CreateEvent()로 이벤트객체 생성.

시그널드 : 대기하고 있는 쓰레드가 이벤트객체를 획득할 수 있는 상태

넌그시널드:대기하고 있는 쓰레드가 이벤트 객체를 획득할 수 없는 상태

 

 

(1)수동 리셋모드 이벤트

넌시그널드 ------SetEvent-------------------> 시그널드 ("데이터 가져다 놓았다 소비자야.일해!!") ( 수동)

넌시그널드 <-----ResetEvent------------------시그널드  ("소비자야 이제 대기해") (수동)

 

-수동으로 다 해줘야된다.

-소비자:WaitForSingleObject로 대기중

만약에 두 개 이상의 소비자쓰레드가 존재한다고 할 때 수동모드에서는 시그널상태가 되면 WaitForSingleObject에서

대기중인 두 개 이상의 소비자쓰레드가 WaitForSingleObject를 빠져나와서 동시에 깨어나게 된다.

 

 

-관찰중인 두 쓰레드가 동시에 깨어나야 할때

 

(2)자동 리셋모드 이벤트

넌시그널드 ------SetEvent-------------------> 시그널드            (수동)

넌시그널드 <-----WaitForSingleObject------------------시그널드 (자동)

-시그널드를 주는 것은 수동이다.SetEvent() - 생산자쓰레드만 알기때문에

-넌시그널드- 시그널드로 바뀌는 것은 자동이다.

 

-한 번에 하나의 쓰레드만 깨어나게 할 때.

 

둘중 하나만실행의 기회를 얻게 된다.

 

3.이벤트 예제 소스

 

쓰레드 -소비자의 역할

메인쓰레드 - 생산자쓰레드의 역할

 

출처:윤성우  -윈도우즈시스템프로그래밍

/*
 StringEvent.cpp
 프로그램 설명: 1. 생산자/소비자 모델의 이해
                2. 동기화 event에 대한 이해.
*/

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <process.h>    /* _beginthreadex, _endthreadex */


unsigned int WINAPI OutputThreadFunction(LPVOID lpParam);

TCHAR string[100];
HANDLE hEvent;

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

 HANDLE  hThread;
 DWORD dwThreadID;

 hEvent = CreateEvent( // event object 생성.
    NULL,  // 상속 불가.
    TRUE,  // manual-reset mode로 생성.
    FALSE,  // non-signaled 상태로 생성.
    NULL  // 이름 없는 event.
    ); 
 if(hEvent==NULL){
  _fputts(_T("Event object creation error \n"), stdout); 
  return -1;
 }
 
 hThread = (HANDLE)_beginthreadex (
      NULL, 0, 
      OutputThreadFunction, 
      NULL, 0, 
      (unsigned *)&dwThreadID
     );

 if(hThread==0) {
  _fputts(_T("Thread creation error \n"), stdout); 
  return -1;
 } 

 _fputts(_T("Insert string: "), stdout); 
 _fgetts(string, 30, stdin);

 SetEvent(hEvent); // event의 state를 signaled 상태로 변경.

 WaitForSingleObject(hThread, INFINITE);
  
  CloseHandle(hEvent); // event 오브젝트 소멸
 CloseHandle(hThread);

    return 0;
}

unsigned int WINAPI OutputThreadFunction(LPVOID lpParam)
{

  WaitForSingleObject(hEvent, INFINITE); // event가 signaled 상태가 되기를 기다린다.

  _fputts(_T("output string: "), stdout); 
  _fputts(string, stdout);

  return 0;
}

 

 

/*
 StringEvent3.cpp
 프로그램 설명: event, mutex 동시 사용 사례.
*/

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <process.h>    /* _beginthreadex, _endthreadex */


unsigned int WINAPI OutputThreadFunction(LPVOID lpParam);
unsigned int WINAPI CountThreadFunction(LPVOID lpParam);

typedef struct _SynchString
{
 TCHAR string[100];
 HANDLE hEvent;
 HANDLE hMutex; 
} SynchString;

SynchString gSynString;

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

 HANDLE  hThreads[2];
 DWORD dwThreadIDs[2];

 gSynString.hEvent = CreateEvent( 
       NULL,  
       TRUE,  
       FALSE,  
       NULL  
      ); 

 gSynString.hMutex = CreateMutex ( 
       NULL,
       FALSE,
       NULL
      );   

 if(gSynString.hEvent==NULL || gSynString.hMutex==NULL) {
  _fputts(_T("kernel object creation error \n"), stdout); 
  return -1;
 }


 hThreads[0] = (HANDLE)_beginthreadex (
      NULL, 0, 
      OutputThreadFunction, 
      NULL, 0, 
      (unsigned *)&dwThreadIDs[0]
     );

 hThreads[1] = (HANDLE)_beginthreadex (
      NULL, 0, 
      CountThreadFunction, 
      NULL, 0, 
      (unsigned *)&dwThreadIDs[1]
     );


 if(hThreads[0]==0 ||hThreads[1]==0) 
 {
  _fputts(_T("Thread creation error \n"), stdout); 
  return -1;
 } 

 _fputts(_T("Insert string: "), stdout); 
 _fgetts(gSynString.string, 30, stdin);

 SetEvent(gSynString.hEvent); // event의 state를 signaled 상태로 변경.

 WaitForMultipleObjects ( 
    2,           // 배열의 길이.
    hThreads,     // 핸들의 배열.
    TRUE,        // 모든 핸들이 신호받은 상태로 될 때 리턴.
    INFINITE  // 무한 대기.
 ); 
  
  CloseHandle(gSynString.hEvent);
  CloseHandle(gSynString.hMutex);
 CloseHandle(hThreads[0]);
 CloseHandle(hThreads[1]);

    return 0;
}

unsigned int WINAPI OutputThreadFunction(LPVOID lpParam)
{

  WaitForSingleObject(gSynString.hEvent, INFINITE); // event가 signaled 상태가 되기를 기다린다.
  WaitForSingleObject(gSynString.hMutex, INFINITE);

  _fputts(_T("Output string: "), stdout); 
  _fputts(gSynString.string, stdout);

  ReleaseMutex(gSynString.hMutex);

  return 0;
}

unsigned int WINAPI CountThreadFunction(LPVOID lpParam)
{

  WaitForSingleObject(gSynString.hEvent, INFINITE); // event가 signaled 상태가 되기를 기다린다.
  WaitForSingleObject(gSynString.hMutex, INFINITE);

  _tprintf(_T("Output string length: %d \n"), _tcslen(gSynString.string)-1);

  ReleaseMutex(gSynString.hMutex);

  return 0;
}

 

4.수동/자동 리셋 타이머

 

(1)타이머 생성

HANDLE CreateWaitableTimer

(LPSECURITY_ATTRIBUTES lpAtrri,

BOOL bManual,                                //모드 설정 (수동이면 최초 한번만 실행, 자동이면 최초 실행+ 일정간격반복)

LPCTSTR lpName

);

 

(2)타이머 설정

BOOL SetWaitableTimer

(HANDLE hTimer,

const LARGE_INTEGER* pDueTime, //최초 몇 초 뒤에(처음 실행에 대한 설정) 

LONG lPeriod, //반복은 몇 초 간격으로 (반복에 대한 설정) - 여기가 0이면 한번만 실행하고 종료한다.

PTIMERAPCROUTINE pfnCompletionRoutine, //실행할 함수

LPVOID  lpArgToCompletionRoutine,             //실행할 함수의 전달인자

BOOL fResume,                                         //전원관리와 관련있는 매개변수(일반적으로 FALSE를 쓰자)

);

//설정사항

-몇 초 뒤에

-반복

 

수동 리셋 타이머 : 일정 시간후 특정 함수 호출

자동 리셋 타이머 : 수동 + 반복

 

 

5.타이머 예제소스

 

(1)수동리셋타이머

/*
 ManualResetTimer.cpp
 프로그램 설명: 수동 리셋 타이머 오브젝트에 대한 이해.
*/

#define _WIN32_WINNT 0x0400

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>


int _tmain(int argc, TCHAR* argv[])
{
    HANDLE hTimer = NULL;
    LARGE_INTEGER liDueTime;

    liDueTime.QuadPart=-100000000;//나노세컨드단위로 설정(현재 시점을 기준으로 10초 후에)

    hTimer = CreateWaitableTimer(NULL, TRUE, _T("WaitableTimer"));
    if (!hTimer)
    {
        _tprintf( _T("CreateWaitableTimer failed (%d)\n"), GetLastError());
        return 1;
    }

    _tprintf( _T("Waiting for 10 seconds...\n"));

    SetWaitableTimer(hTimer, &liDueTime0, NULL, NULL, FALSE); //0이므로 반복하지 않는다.

     WaitForSingleObject(hTimer, INFINITE);
    _tprintf( _T("Timer was signaled.\n") );
    MessageBeep(MB_ICONEXCLAMATION);

    return 0;
}

 

(2)자동리셋타이머

/*
 PeriodicTimer.cpp
 프로그램 설명: 주기적 타이머에 대한 이해.
*/

#define _WIN32_WINNT 0x0400

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>


int _tmain(int argc, TCHAR* argv[])
{
    HANDLE hTimer = NULL;
    LARGE_INTEGER liDueTime;

    liDueTime.QuadPart=-100000000;

    hTimer = CreateWaitableTimer(NULL, FALSE, _T("WaitableTimer"));//자동리셋타이머
    if (!hTimer)
    {
        _tprintf( _T("CreateWaitableTimer failed (%d)\n"), GetLastError());
        return 1;
    }

    _tprintf( _T("Waiting for 10 seconds...\n"));

    SetWaitableTimer(hTimer, &liDueTime, 5000, NULL, NULL, FALSE); //5초마다 반복

    while(1)
   {
      WaitForSingleObject(hTimer, INFINITE);
      _tprintf( _T("Timer was signaled.\n") );
      MessageBeep(MB_ICONEXCLAMATION);
   }
    return 0;
}

 

6.참고자료

SetWaitableTimer

The SetWaitableTimer function activates the specified waitable timer. When the due time arrives, the timer is signaled and the thread that set the timer calls the optional completion routine.

BOOL SetWaitableTimer(
  HANDLE hTimer,
  const LARGE_INTEGER* pDueTime,
  LONG lPeriod,
  PTIMERAPCROUTINE pfnCompletionRoutine,
  LPVOID lpArgToCompletionRoutine,
  BOOL fResume
);

Parameters

hTimer
[in] Handle to the timer object. The CreateWaitableTimer or OpenWaitableTimer function returns this handle.

The handle must have the TIMER_MODIFY_STATE access right. For more information, see Synchronization Object Security and Access Rights.

pDueTime
[in] Time after which the state of the timer is to be set to signaled, in 100 nanosecond intervals. Use the format described by theFILETIME structure. Positive values indicate absolute time. Be sure to use a UTC-based absolute time, as the system uses UTC-based time internally. Negative values indicate relative time. The actual timer accuracy depends on the capability of your hardware. For more information about UTC-based time, see System Time.
lPeriod
[in] Period of the timer, in milliseconds. If lPeriod is zero, the timer is signaled once. If lPeriod is greater than zero, the timer is periodic. A periodic timer automatically reactivates each time the period elapses, until the timer is canceled using theCancelWaitableTimer function or reset using SetWaitableTimer. If lPeriod is less than zero, the function fails.
pfnCompletionRoutine
[in] Pointer to an optional completion routine. The completion routine is application-defined function of type PTIMERAPCROUTINE to be executed when the timer is signaled. For more information on the timer callback function, see TimerAPCProc.
lpArgToCompletionRoutine
[in] Pointer to a structure that is passed to the completion routine.
fResume
[in] If this parameter is TRUE, restores a system in suspended power conservation mode when the timer state is set to signaled. Otherwise, the system is not restored. If the system does not support a restore, the call succeeds, but GetLastError returns ERROR_NOT_SUPPORTED.

Return Values

If the function succeeds, the return value is nonzero.

If the function fails, the return value is zero. To get extended error information, call GetLastError.

Remarks

Timers are initially inactive. To activate a timer, call SetWaitableTimer. If the timer is already active when you callSetWaitableTimer, the timer is stopped, then it is reactivated. Stopping the timer in this manner does not set the timer state to signaled, so threads blocked in a wait operation on the timer remain blocked.

When the specified due time arrives, the timer becomes inactive and the APC is queued to the thread that set the timer. The state of the timer is set to signaled, the timer is reactivated using the specified period, and the thread that set the timer calls the completion routine when it enters an alertable wait state. For more information, see QueueUserAPC.

If the thread that set the timer exits before the timer elapses, the timer is cancelled. If you call SetWaitableTimer on a timer that has been set by another thread and that thread is not in an alertable state, the completion routine is cancelled.

When a manual-reset timer is set to the signaled state, it remains in this state until SetWaitableTimer is called to reset the timer. As a result, a periodic manual-reset timer is set to the signaled state when the initial due time arrives and remains signaled until it is reset. When a synchronization timer is set to the signaled state, it remains in this state until a thread completes a wait operation on the timer object.

 

 

TimerAPCProc

The TimerAPCProc function is an application-defined timer completion routine. Specify this address when calling theSetWaitableTimer function. The PTIMERAPCROUTINE type defines a pointer to this callback function. TimerAPCProc is a placeholder for the application-defined function name.

VOID CALLBACK TimerAPCProc(
  LPVOID lpArgToCompletionRoutine,
  DWORD dwTimerLowValue,
  DWORD dwTimerHighValue
);

Parameters

lpArgToCompletionRoutine
[in] Value passed to the function using the lpArgToCompletionRoutine parameter of the SetWaitableTimer function.
dwTimerLowValue
[in] Low-order portion of the UTC-based time at which the timer was signaled. This value corresponds to the dwLowDateTimemember of the FILETIME structure. For more information about UTC-based time, see System Time.
dwTimerHighValue
[in] High-order portion of the UTC-based time at which the timer was signaled. This value corresponds to the dwHighDateTimemember of the FILETIME structure.

Return Values

This function does not return a value.

Remarks

The completion routine is executed by the thread that activates the timer using SetWaitableTimer. However, the thread must be in an alertable state. For more information, see Asynchronous Procedure Calls.

반응형

+ Recent posts