컴퓨터 공학/C++

[c/c++] Boost 주요 기능 정리

혼새미로 2018. 9. 12. 20:26
반응형

boost의 주요 기능들과 그 기능들을 활용하여 구현한 채팅 프로그램에 대해 설명드리겠습니다. 주요 기능들은 다음과 같습니다.

Boost::thread

Boost::bind

boost::function

boost::chrono

스마트 포인터

boost::signal

boost::mutex

boost::asio

boost::timer

먼저, 스레드에 대해 설명드리겠습니다. 스레드는 프로세스 내에서 실행되는 흐름의 단위라고 하는데요, 멀티스레드를 사용함으로써 프로세스의 처리성능을 높일 수 있는 장점이 있습니다. boost에서 제공하는 스레드를 사용하기 위해 boost/thread.hpp를 선언해야 하며, 다른 클래스와 다르게 스레드는 복사가 불가능하다는 특징이 있습니다. 스레드를 사용하는 예제가 다음의 코드로 나타내고 있습니다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
void f(){
 
  cout<<”thread call\n”;
 
}
 
void main(){
 
  boost::thread t1(f);//스레드 변수 초기화 및 실행
 
  t1.join(); //스레드 종료 시 까지 대기
 
}
cs

스레드 변수를 선언하면서 스레드가 수행할 함수를 같이 선언하고, 선언이 끝나자 마자, 스레드는 시작합니다. 만약 해당 스레드가 끝날 때 까지 코드의 특정 위치에서 대기시키고 싶다면, 다음과 같이 join() 함수를 호출하면 됩니다. 스레드 함수가 종료된 후에 join() 함수의 다음 줄을 수행합니다. 만약 함수가 종료되지 않더라도 일정 시간 동안만 대기하고자 한다면 timed_join 함수를 호출할 수 있습니다. 

 t1.timed_join(boost::posix_time::seconds(2));

인자 값으로 boost에서 제공하는 시간 관련 모듈인 posix_time을 통해 원하는 시간 값을 넘겨줄 수 있습니다. 스레드에서 join()함수는 한번만 유효한데, 만약 현재 join()함수가 호출 가능한지 확인하려면 joinable() 함수를 사용하면 됩니다.

또한, 스레드 함수 내에서도 일정시간 동안 대기하고 싶은 경우가 있을 수 있는데요, 이를 위해 boost::this_thread::sleep()함수를 사용할 수 있습니다. 인자 값으로 posix_time의 값을 넘겨주면 됩니다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void f(){
 
  boost::this_thread::sleep(boost::posix_time::seconds(1)); //1초 대기
 
  cout<<”thread call\n”;
 
}
 
void main(){
 
  boost::thread t1(f);//스레드 변수 초기화 및 실행
 
  t1.join(); //스레드 종료 시 까지 대기
 
}
cs

또한, 각 스레드를 구분하기 위해 스레드마다 고유한 id가 존재하는데, 이 id 값을 반환하는 함수는 get_id() 함수입니다.

 boost::thread t1();

t1.get_id();

부가적으로, 스레드를 참조하는 변수가 있을 때, 해당 변수가 참조하는 스레드를 해제하고 다른 스레드를 참조하고자 한다면 detach() 함수를 사용하면 됩니다. 

 boost::thread t1();

t1.detach(); //스레드 참조 해제

또한, 서로 다른 두 스레드를 참조하고 있는 두 변수 t1과 t2가 있을 때, 서로의 스레드를 교환하고 싶은 경우에는 swap() 함수를 사용하면 됩니다. 

 t1.swap(t2);

마지막으로, 다수의 스레드를 미리 생성해놓고, 필요할 때 마다 선점해서 사용하는 방식인 스레드 풀을 위한 클래스인 thread_group을 사용할 수 있습니다. 

1
2
3
4
boost::thread_group m_group;
for(int i=0;i<100;i++){
  m_group.create_thread(boost::bind(&boost::asio::io_service::run, &m_io_service));
}
cs

다음 예제와 같이 반복문을 통해 create_thread 함수를 호출하면 스레드가 생성되어 thread_group에 귀속되며, 스레드를 생성할 때마다 io_service 변수를 넘겨준 후에 io_service의 post()를 통해 bind 함수를 넘겨주면 해당 함수가 스레드 풀 중에서 남는 스레드를 선택하여 작업을 수행합니다.

---

다음으로 bind에 대해 설명드리겠습니다. 특정 함수를 호출할 때 그 매개변수 값을 함께 넘겨주기 위해 bind를 사용할 수 있습니다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int f(int a,int b){
 
  return a+b;
 
}
 
int g(int a, int b, int c){
 
  return a+b+c;
 
}
 
void main(){
 
  auto p1=bind(f,1,2);
 
  auto p2=bind(g,1,2,3);
 
  cout<<p1()<<“,”<<p2()<<endl;
 
}
cs

다음 예제와 같이 두 함수 f와 g가 있을 때, 변수 p1에는 f라는 함수를 호출하고 인자 값으로 1,2를 무조건 넘겨주는 함수를 가리키도록 하고, p2는 g함수에 인자값 1,2,3을 넘겨주도록 지정하였습니다. 그 후에 변수이름과 소괄호만 붙여주면 인자값이 지정된 함수가 바로 호출되는 방식입니다. 

만약 bind 함수를 사용할 때, 상수값이 아니라 보관하고 있는 변수의 값을 지정하고자 한다면 다음의 예제와 같이 변수명을 인자 값으로 넘겨주면 됩니다.

 bind(f,1,x);

그런데, 여기서 넘겨준 x값은 복사를 통해 보관되기 때문에 bind를 지정하고 난 후에 x값을 변경하더라도 그 bind 내의 값은 유지됩니다. 만약 x의 참조 값을 사용하고 싶다면 다음 예제와 같이 boost::ref() 함수를 사용하여 지정할 수 있습니다. 

 bind(f,1,boost::ref(x));

다른 방법으로, 인자에 placeholder를 만들어놓고, bind를 호출하면서 그 값을 넘겨주는 방법도 사용할 수 있습니다. 

 auto p1 = bind(f, _1, _2);

cout<<p1(3,5)<<endl; // 3+5=8 반환


bind는 함수 뿐만 아니라 연산자 오버로딩을 한 구조체에서도 사용할 수 있는데요, 다음과 같이 F라는 구조체 내에 두 함수가 있습니다. 

1
2
3
4
5
6
7
8
9
10
11
12
struct F{
 
  int operator()(int a,int b){return a-b;}
 
  bool operator()(long a, long b) {return a==b;}
 
};
 
F f;
 
int x =104;
bind<int>(f, _1, _1)(x);
cs

하나는 int형으로 두 인자 값을 빼고, 다른 함수는 long 타입으로 같은 값인지 비교합니다. 이렇게 해서 bind함수를 사용하면서 사용되는 타입을 꺽쇠괄호로 지정하고 구조체 변수를 지정하면 구조체 내의 해당 타입을 사용하는 함수가 호출됩니다.

---

다음으로, function에 대해 설명드리겠습니다. function은 함수포인터를 정의할 수 있는 라이브러리입니다. 다음 예제와 같이, 꺽쇠괄호로 반환 타입, 인자 값들의 개수와 타입을 지정할 수 있습니다. 

1
2
3
4
5
6
7
8
9
nt f(int a, int b){reutnr a+b;}
 
void main(){
 
  boost::function<int (int,int)> fp= f;
 
  cout<<fp(1,2)<<endl;
 
}
cs

이렇게 정의한 function 변수는 반환 타입과 인자 타입이 같은 함수들을 모두 가리킬 수 있습니다. 

또한, function을 사용하여 bind를 참조할 수 있습니다. 다음과 같이 bind에 placeholder를 지정하고 function 변수를 통해 1과 2를 인자 값으로 지정할 수 있습니다. 

 boost::function<int (int,int)> fp = boost::bind(f, _1, _2);

cout<<fp(1,2)<<endl;

또한, 하나의 함수에 대해 한번에 여러 개의 인자값들을 계산하기 위해 벡터를 사용할 수 있는데요, 먼저 벡터에 인자 값으로 사용될 값들을 삽입한 후에, for_each 함수에 벡터의 시작점과 끝점, 그리고 bind로 함수와 인자값을 지정해주면 각 인자 값에 대한 함수가 순차적으로 실행됩니다.

1
2
3
4
5
6
7
8
9
vector<int> tv;
 
tv.push_back(1);
 
tv.push_back(4);
 
tv.push_back(3);
 
for_each(tv.begin(), tv.end(), boost::bind(f,1,_1));
cs

---

다음으로 chrono에 대해 설명하겠습니다. chrono는 정밀한 시간 측정을 위한 라이브러리입니다. 사용하는 방법은 코드의 해당 위치에 시간을 마킹을 하는 방식입니다. 다음 예제와 같이 어떤 작업을 수행하기 전에 system_clock의 time_point 로 코드를 실행할 때의 시간을 마킹해놓고, 어떤 작업을 수행한 후에 마찬가지로 시간을 마킹한 후에 두 시간의 차이를 구합니다. 

1
2
3
4
5
6
7
system_clock::time_point start = system_clock::now();
 
//어떤 작업을 수행
 
system_clock::time_point end = system_clock::now();
 
duration<double> sec = end – start;
cs

반환 값은 duration이라는 클래스인데요, 이 클래스를 통해 나노 초부터 시간까지 원하는 단위로 값을 출력할 수 있다는 특징이 있습니다. 

아래에 보시면 duration 클래스에서 꺽쇠괄호의 두번째 타입에 nano, micro, milli 등의 타입을 지정해 놓은 것을 알 수 있습니다.

  • typedef duration<int, ratio<3600> > hours;
  • typedef duration<int, ratio<60> > minutes;
  • typedef duration<long long> > seconds;
  • typedef duration<long long, milli > milliseconds;
  • typedef duration<long long, micro > microseconds;
  • typedef duration<long long, nano > nanoseconds;

now 함수를 통해 마킹하는 시간정보는 가장 정밀도가 높은 나노를 기준으로 저장하기 때문에 그 아래의 시간들로 변활 할 때는 다음과 같이 명시적 형변환을 수행해야 합니다. 

  • hours hour = duration_cast<hours>(end - start);
  • minutes min = duration_cast<minutes>(end - start);
  • milliseconds milliSec = duration_cast<microseconds>(end - start);
  • nanoseconds nanoSec = end - start;


duration_case라는 캐스트 연산자를 사용하여 꺽쇠괄호로 원하는 시간 타입을 지정할 수 있습니다. system_clock으로 시간을 측정하면, 운영체제의 시간이 앞으로 이동했을 때, system_clock의 시간도 같이 변경된다는 문제점이 있습니다. 이러한 현상을 피하기 위해 steady_clock을 사용할 수 있습니다.

 steady_clock::time_point start = steady_clock::now();

---

다음으로 스마트포인터에 대해 설명드리겠습니다. 개발자가 코드를 작성하면서 특정 함수 내에서 어떤 객체를 동적으로 할당했는데, throw가 발생하거나 깜빡하고 해제를 안 하면 메모리 누수가 발생하게 됩니다. 이와 같은 메모리 누수의 여지를 없애기 위해 함수 내에 정적으로 정의된 스마트 포인터에 동적 할당된 객체 정보를 보관해두고, 함수가 종료되는 시점에서 스마트 포인터의 소멸자에서 보관중인 동적 객체의 메모리를 해제하는 기법입니다. boost에서 제공하는 스마트 포인터는 크게 여섯 종류가 있습니다. 

  •  shared_ptr : T 객체가 사용 중인지 판단하기 위해 참조 카운팅 방식을 사용
  • scoped_ptr : 변수의 범위를 벗어나는 순간 자동으로 삭제되는 스마트 포인터. 대입 연산을 할 수 없으며 보통 포인터에 비해 속도 저하가 없음
  • intrusive_ptr : shared_ptr처럼 참조 카운트 방식을 사용하지만 속도는 shared_ptr보다 빠름. 단, T 타입 자체에 참조 카운트를 위한 기법이 포함되어 있어야 함
  • weak_ptr : shared_ptr과 함께 사용되어 순환 참조 현상을 방지하는데 사용
  • shared_array : shared_ptr과 비슷하지만 T 타입의 배열을 저장하는데 사용
  • scoped_array : scoped_ptr과 비슷하지만 T 타입의 배열을 저장하는데 사용

shared_ptr은 동적으로 할당된 객체가 사용 중인지 판단하기 위해 참조 카운트를 사용합니다. 두번째로 scoped_ptr은 스마트 포인터 변수가 사라지는 시점에서 동적 할당된 객체도 같이 제거하는 방식으로, 대입연산이 불가능하고 일반 포인터에 비해 속도 저하가 적습니다. intrusive_ptr은 shared_ptr과 같이 참조 카운팅 방식을 사용하지만 속도는 shared_ptr보다 빠르다고 합니다. 대신, 참조 카운트를 위한 함수를 직접 정의해주어야 합니다. weak_ptr은 shared_ptr과 함께 사용되어 순환 참조 현상을 방지하는데 사용합니다. shared_array와 scoped_array는 shared_ptr, scoped_ptr과 비슷하지만 배열을 저장하는데 사용합니다.


가장 간단한 스마트 포인터인 scoped_ptr에 대해 설명드리겠습니다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyClass{
 
  public:
 
    int h;
 
    void Use(){
 
      cout<<“Hello class\n”;
 
    }
 
};
 
void main(){
 
  boost::scoped_ptr<MyClass> scPtr(new MyClass);
 
  scPtr->Use();
 
//함수 종료 시점에서 동적 할당된 객체가 제거됨
cs

다음과 같이 MyClass라는 클래스가 존재할 때, scoped_ptr의 꺽쇠괄호로 지정할 객체의 타입을 지정하고, 변수를 선언함과 동시에 괄호로 동적으로 할당된 객체 정보를 넘겨줍니다. 그 후에 스마트포인터 변수로 해당 객체를 자유롭게 사용할 수 있습니다. 그리고 함수가 종료되는 시점에서 scoped_ptr의 소멸자가 호출되고 이때 MyClass 객체가 제거됩니다.


shared_ptr은 해당 객체를 가리키는 포인터의 개수를 보관하는 방법입니다. 즉, 객체를 가리키는 포인터의 개수가 0이 되면 해당 객체는 자동으로 제거됩니다. 

1
2
3
4
5
6
7
8
9
 void Func(){
 
  boost::shared_ptr<CSample> mySample(new CSample); //count 1
 
  boost::shared_ptr<CSample> mySample2 = mySample; //count 2
 
  mySample.reset();// count1
 
}//count 0
cs

다음 예시에서, CSample 객체를 새로 할당하여 mySample에서 참조를 하고 있습니다. 두번째 라인에서 mySample2가 mySample이 가리키는 객체 정보를 받아 가리키게 되어 카운트가 2가 됩니다. 세번째 라인에서 mySample의 reset 함수를 호출하면 카운트 개수가 감소하여 다시 1이 되고, 함수가 종료되는 시점에서 모든 스마트포인터가 사라지므로 카운트가 0이 되어 해당 객체가 제거됩니다.


스마트포인터가 참조하는 객체가 클래스로 정의된 경우에 클래스 내부에서 전역 함수를 호출할 때 인자로 this를 넘기면 에러가 발생합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class SPtrTest{
 
  public:
 
    int var1;
 
    void operation(){ 
 
      Func(this);
 
    }
 
};
 
void Func(shared_ptr<SPtrTest> sp){
 
  sp->var1++;
 
};
 
void main(){
 
  shared_ptr<SPtrTest> tc(new SPtrTest);
 
  tc->var1 = 0;
 
  tc->operation();
 
}
cs

그러면 다음과 같이 operation() 함수 내에 shared_ptr<SPtrTest> 변수를 하나 생성해서 this를 가리킨 후에 Func의 인자로 넘겨주는 방법을 생각해볼 수 있습니다. 그러나, shared_ptr에서 참조 카운트가 증가되는 방법은 같은 스마트포인터 끼리의 대입에서만 유효합니다. 위에서처럼 this는 객체 자신이기 때문에 참조카운트는 1이 됩니다. 그렇기 때문에 operation()과 Func()에서 함수가 끝나면 두번의 소멸자가 호출되는데 이는 에러를 발생시킵니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 class SPtrTest{
 
  public:
 
    int var1;
 
    void operation(){ 
 
      shared_ptr<SPtrTest> thisPtr(this);
 
      Func(thisPtr);
 
    }
 
};
 
void Func(shared_ptr<SPtrTest> sp){
 
  sp->var1++;
 
};
 
void main(){
 
  shared_ptr<SPtrTest> tc(new SPtrTest);
 
  tc->var1 = 0;
 
  tc->operation();
 
}
cs

이러한 문제를 해결하기 위해 BOOST에서는 enable_shared_from_this라는 클래스를 상속받아 사용하는 것을 권하고 있습니다. 위와 같이 대상이 되는 클래스에 enable_shared_from_this<T>를 상속받은 후에, 만약 객체 내에서 전역함수를 호출하게 된다면 인자로 넘겨줄 때 shared_from_this() 함수를 호출하면 참조 카운트가 증가되는 자신을 넘겨줄 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class SPtrTest : public boost::enable_shared_from_this<SPtrTest>{
 
  public:
 
    int var1;
 
    void operation(){ 
 
      Func(shared_from_this());
 
    }
 
};
 
void Func(shared_ptr<SPtrTest> sp){
 
  sp->var1++;
 
};
 
void main(){
 
  shared_ptr<SPtrTest> tc(new SPtrTest);
 
  tc->var1 = 0;
 
  tc->operation();
 
}
cs

다음으로 weak_ptr에 대해 설명드리겠습니다. shared_ptr은 자기 자신을 참조하여 복사하는 순환 참조가 발생할 수 있습니다. 이때 참조는 하되, 객체의 수명에는 관여하지 않는 스마트 포인터를 사용하기 위해 weak_ptr을 사용합니다. weak_ptr은 shared_ptr을 참조할 때, weak_ 참조 카운트는 증가시키되, strong 참조 카운트는 증가시키지 않습니다. weak 참조 카운트는 객체의 소멸에 관여하지 않습니다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
shared_ptr<int> sp1(new int(5)); //strong ref :1, weak ref : 0
 
weak_ptr<int> wp1 = sp1; //strong ref :1, weak ref: 1
 
{
 
  shared_ptr<int> sp2 = wp1.lock(); //strong ref: 2, weak ref: 1
 
  if(sp2){
 
    //weak_ptr의 객체에 접근하기 위한 방법
 
  }
 
//strong ref: 1, weak ref: 1
 
sp1.reset(); //strong ref: 0, 즉 sp1 소멸되고 wp1이 참조하던 sp1이 소멸되었으므로 wp1은 expired
 
 
 
shared_ptr<int> sp3 = wp1.lock(); //expired된 wp1은 참조하고 있는 shared_ptr이 없으므로 sp3 도 empty
 
if(sp3){
 
  //이 블록은 실행되지 않음
 
}
cs

다음 예시 코드에서 보시면 shared_ptr의 객체를 weak_ptr이 두번째로 참조하고 있습니다. 이떄, strong 카운트 1, weak 카운트 1이 됩니다. 다음에 shared_ptr인 sp2가 weak_ptr로부터 객체를 참조하기 위해 lock()함수를 통해 객체를 받습니다. 다음에 if문으로 sp2가 가리키는 객체가 유효한지 확인을 하고 유효하면 그제서야 사용할 수 있습니다. 만약 sp1.reset()이 호출되어 sp1이 가리키는 객체가 소멸되었다면 wp1이 가리키는 객체가 만료되었기 때문에 새롭게 선언한 sp3가 wp1으로부터 객체를 받아도 유효하지 않기 때문에 실행이 되지 않습니다. weak_ptr은 클래스에서 부모가 자식을 가리키고 자식이 부모를 가리키는 순환 참조를 막을 때 특히 유용합니다.


여러 객체의 정보를 사용하는 vecto나 list를 스마트 포인터와 함께 사용하면 메모리 해제를 신경쓸 필요가 없어집니다. 

1
2
3
4
5
6
7
typedef boost::shared_ptr<MyClass> MyClassPtr;
 
vector<MyClassPtr> vec;
 
vec.push_back(MyClassPtr(new MyClass(“bigString”)));
 
...//사용, 메모리 해제 신경쓸 필요 없음
cs

다음 예시와 같이 typdef를 통해 특정 클래스의 shared_ptr를 정의한 후에 vector의 타입으로 지정해 놓으면 , 벡터에 새로운 객체를 삽입할 때 동적으로 객체를 생성하면서 스마트포인터와 함께 넘겨주면 나중에 벡터 내의 객체를 일일이 제거할 필요가 없어집니다.

---

다음으로 시그널에 대해 설명드리겠습니다. 함께 호출될 함수들을 시그널에 등록하고 시그널을 한번만 호출하면 등록된 순서대로 함수들을 실행할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 void print_sum(float x, float y){
 
  cout<<“print_sum:”<<x+y<<endl;
 
}
 
void print_product(float x, float y){
 
  cout<<“print_product:” <<x*y<<endl;
 
}
 
void print_difference(float x, float y){
 
  cout<<“print_difference:” <<x-y<<endl;
 
}
 
void print_quotient(float x, float y){
 
  cout<<“print_quotient:”<<x/y<<endl;
 
}
 
void main(){
 
  boost::signal<void (float,float)> sig;
 
 sig.connect(print_sum);
 
 sig.connect(print_product);
 
 sig.connect(print_difference);
 
 sig.connect(print_quotient);
 
  sig(5,3);
 
}
cs

다음 예시와 같이 덧셈, 뺄셈, 곱셈, 나눗셈의 함수를 하나의 시그널로 등록하기 위해 반환타입이 void 인자 타입이 float로 하는 시그널을 생성하고 네 개의 함수들을 connect 함수를 통해 차례대로 등록합니다. 그리고 시그널 함수를 인자 값과 함께 넘겨주면 등록된 함수들이 순차적으로 실행됩니다.



만약 시그널에 등록된 함수들 중에서 특정 함수를 우선적으로 호출하려면 다음 과같이 첫 번째 인자에 순번을 지정할 수 있습니다. 

1
2
3
4
5
boost::signal<void ()> sig;
sig.connect(1, print_sum);
sig.connect(0, print_product);
sig.connect(3, print_difference);
sig.connect(2, print_quotient);
cs

그러면 등록된 순서와 상관없이 순번이 낮은 순서대로 함수들이 실행됩니다.

시그널에 등록된 함수들의 반환 값을 받아서 처리하는 함수를 만들 수 있습니다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<typename T>
struct maximum{
  typedef T result_type;
  template<typename InputIterator>
  T operator(InputIterator first, InputIterator last) const//최대값 반환 함수
    if(first == last)
      return T();
    T max_value = *first++;
    while(first != last){
      if(max_value < *first)
        max_value = *first;
      ++first;
    }
    return max_value;
  }
};
boost::signal<float (float x, float y), maximum<float> > sig;
cs

다음의 예시는 시그널에 등록된 함수들의 반환 값을 받아서 그 중 최대값을 반환하는 함수가 포함된 구조체입니다. 중간에 연산자 오버로딩을 해서 시그널에 등록된 첫번째 함수 위치부터 마지막 함수 위치까지 반복하면서 max_value라는 변수에 최대값을 계산하여 저장합니다.

---

다음으로 뮤텍스에 대해 설명하겠습니다. 멀티스레드 환경에서 동시에 공유 자원에 접근하여 데이터를 변경하면 데이터 불일치가 발생할 수 있습니다. 이때, mutex를 사용하여 한 번에 하나의 스레드만 접근하여 공유자원을 사용하도록 제어할 수 있는데요, boost에서 제공하는 mutex를 사용하기 위해서는 직접 잠그는 방식이 아니라 간접적으로 잠그고 푸는 방식을 사용하는데 이를 scoped lock이라고 부릅니다. 위에서 설명한 scoped_ptr과 같이 변수의 범위를 벗어나면 소멸자에 의해 제거되는 방식을 사용합니다. 이렇게 함으로써 데드럭을 방지할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
boost::mutex g_mutex; //전역변수
void Function(int nNumber){
  char szMessage[128]={0,};
  sprintf_s(szMessage, 128-1, “%s(%d) | time(%d)”, __FUNCTION__, nNumber, time(NULL));
  {
    boost::mutex::scoped_lock lock(g_mutex);
    cout<<”워커스레드 id:” << GetCurrentThreadId() <<”. “ <<szMessage<<endl;
  }
  Sleep(3000);
}
cs

다음 예시에서, 멀티스레드가를 제어하기 위해 뮤텍스 변수는 전역변수를 선언하여 사용합니다. 그리고 다음 예시와 같이 Function 이라는 함수를 멀티스레드가 동시에 접근할 때, 중간의 블록이 있는 부분에 오직 하나의 스레드만 사용하도록 만들기 위해 mutex::scoped_lock 변수를 정의하고 인자 값으로 뮤텍스를 넘겨줍니다. lock 변수는 블록 내에서 유효하다가 블록이 끝나는 시점에서 사라지고 다시 뮤텍스를 사용할 수 있게 됩니다. 만약 뮤텍스가 사용 중이라면 다른 스레드들은 그 위치에 대기하게 됩니다.

mutex를 사용할 때, 특정 조건을 만족할 때 까지 대기해야 하는 경우가 있습니다. 예를 들어, 버퍼에 데이터를 삽입하는 스레드와 추출하는 스레드가 동시에 진행되고 있을 때, 만약 버퍼가 가득 찼을 경우, 삽입하는 스레드는 버퍼에 빈공간이 생길 때 까지 대기를 해야 합니다. 반대로, 추출 스레드가 버퍼에서 데이터를 추출하려고 하는데, 버퍼에 데이터가 하나도 없는 경우, 데이터가 삽입될 때까지 대기해야 합니다. 이를 위해 condition 클래스를 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
boost::mutex io_mutex;
boost::mutex mutex;
int buf[BUF_SIZE];
boost::condition cond;
void put(int m){
  scoped_lock lock(mutex); //put과 get 모두에서 사용
  if(full == BUF_SIZE){
    {
      boost::mutex::scoped_lock lock2(io_mutex); //put과 get 모두에서 사용
      cout<<“버퍼가 가득 찼습니다. 대기중...”<<endl;
    }
    while(full == BUF_SIZE){
      cond.wait(lock); //버퍼 공간이 생길 때 까지 대기 get에서 mutex 사용
    }
  }
  buf[p]=m;
  cond.notify_one(); //작업 완료 통보
}
cs

다음 코드는 버퍼에 데이터를 삽입할 때 condition을 사용하는 예시를 나타냅니다. put 함수에서 함수가 시작하자마자 scoped_lock으로 mutex를 잠급니다.  그런데 만약 버퍼가 가득차서 더 이상 삽입할 수 없다면 while문과 같이 공간이 생길 때 까지 condtion 변수의 wait 함수로 lock을 넘겨주어 일시적으로 mutex의 소유권을 제거합니다. 이후에 다른 스레드에서 버퍼의 데이터를 추출하여 공간이 생기면 그 함수에서 cond.notify_one()이라는 함수를 호출하게 되는데, 이를 통해 put 함수에서 대기중이던 스레드는 다음 줄을 실행하게 되고 버퍼에 데이터를 무사히 삽입하게 됩니다.

---

다음으로, asio에 대해 설명드리겠습니다. asio는 주로 소켓 프로그래밍에 사용되며, 비동기 IO가 가능합니다. IO와 같이 시간이 오래 걸리는 처리를 OS의 비동기 기능과 스레드를 사용하여 처리할 수 있습니다.

Linux Kernel 2.4 : select를 사용

Linux Kernel 2.6 : epoll 사용

FreeBSD, Mac OS X : Kqueue 사용

Solaris : /dev/poll 사용

Windows : Overlapped IO와 IO Completion 사용 


asio는 각 운영체제마다 다음과 같이 서로 다른 소켓 라이브러리를 사용하였습니다.

asio에서 가장 중요한 클래스로는 io_service가 있습니다. io_service는 커널에서 발생한 I/O 이벤트를 디스패치 해주는 클래스로, io_serivce를 통해서 커널에서 발생한 네트워크 상 접속 받기, 접속하기, 데이터 받기, 데이터 보내기 이벤트를 알 수 있습니다.

 boost::asio::ip::tcp::endpoint endpoint(boost::asio::ip::tcp::v4(), PORT_NUMBER); 


우선, 서버 측에서 접속을 받기 위해 IP주소 체계와 포트번호를 endpoint 클래스를 통해 설정합니다. 

boost::asio::ip::tcp::acceptor acceptor(io_service, endpoint); 


그리고 클라이언트가 접속했을 때 접속을 받아들이는 클래스인 acceptor를 생성하면서 인자로 io_service와 endpoint를 넘겨줍니다.  

boost::asio::ip::tcp::socket socket(io_service);

acceptor.accept(socket); 


그리고 실제로 acceptor를 통해 접속대기를 하는데 io_service로 직접 접속을 받는 것이 아니라 socket을 생성하여 접속 처리를 합니다. 위의 예제에서는 동기형 방식을 사용하였습니다. 그래서 클라이언트가 접속을 할 때 까지 대기합니다. 

1
2
3
char buf[128];
boost::system::error_code error;
size_t len = socket.read_some(boost::asio::buffer(buf), error);
cs

다음으로, 클라이언트가 보낸 메시지를 받기 위해 socke의 read_some이라는 함수를 사용합니다. error_code 클래스를 통해 각종 에러 정보를 받아 볼 수 있습니다. 

1
2
3
4
char* buf = “hello world”;
int len = strlen(buf);
boost::system::error_code error;
size_t len = socket.write_some(boost::asio::buffer(buf, len), error);
cs

또한 클라이언트에게 메세지를 보낼 때는 write_some이라는 함수를 사용할 수 있습니다.

지금까지 설명한 동기 IO 프로그래밍은 A라는 작업을 요청하면 끝날 때 까지 대기하는 방식입니다. 그에 반해 비동기 IO 프로그래밍은 A라는 작업을 요청한 후에 다음 작업을 진행하다가 요청한 작업 A가 끝나면 완료 통보를 받은 후 관련 작업을 하는 방식입니다. 관련 함수는 보통 앞에 async라고 표기합니다. 그림에서 왼쪽이 동기 IO 방식을 나타내고, 오른쪽이 비동기 IO 방식을 나타냅니다.

--

다음은 asio 중의 하나인 타이머 기능이 있는데요, 이 타이머는 io_service를 받습니다. 그래서 입력한 시간이 지나면 등록한 함수를 실행하도록 할 수 있습니다. 마찬가지로 async_wait을 통해 비동기적으로 수행합니다.

1
2
3
4
5
6
7
8
9
void print(const boost::system::error_code& e){
  cout<<“Hello world!” <<endl;
}
void main(){
  boost::asio::io_service io;
  boost::asio::deadline_timer t(io, boost::posix_time::seconds(5));
  t.async_wait(&print);
  io.run(); //io에 등록된 요청 수행시작
}
cs

만약 등록한 시간을 다시 확인하고 싶다면 expires_from_now()를 호출할 수 있습니다.

1
2
boost::asio::dead_line_timer t(io, boost::posix_time::seconds(5));
cout<<t.expires_from_now().seconds()<<endl;
cs

이상입니다. 감사합니다!



반응형