반응형

BLOG main image





boost 페이지


http://www.boost.org/doc/libs/1_36_0/doc/html/thread/thread_management.html#thread.thread_management.thread.multiple_argument_constructor


를 보면 다음 구문으로 join에 멤버함수에 대한 역할을 알 수 있다




In order to wait for a thread of execution to finish, the join() or timed_join() member functions of the boost::thread object must be used.



번역하기 귀찮은 사람들을 위해 써놓자면


끝내기위한 쓰래드실행을 기다리기위해서, join()  또는 timed_join()   boost::thread 오브젝트의 멤버함수들이 사용되어져야한다.


임을 알 수 있다 ∴  .join() 함수 - 쓰레드가 종료될 때까지 대기하게된다



p.s waitForSingleObject 쯤으로 생각해 두면 될듯, 아래 포스트들은 역시 Boost::thread에 관한 실행 예제들과 좀 더 자세한 멤버들에 대한 내용이다




[condition 과 mutex 의 사용시 주의사항]



        //condition 을 사용하기전에 주의해야하는 점이 아래 나온다

//A condition object is always used in conjunction with a mutex object(an object whose type is a model of a Mutex or one of its refinements). 

//The mutex object must be locked prior to waiting on the condition, which is verified by passing a lock object

        //http://www.boost.org/doc/libs/1_34_0/doc/html/boost/condition.html 에서 발췌

 

      condition 을 사용하기전에 반드시 mutex를 lock 건것에 대한 변수로 wait 해야 된다는것.

     다시말해 condition 은 뮤텍스를 lock 한 객체를 받아야야하고 이것

     _condition.wait(lock); 로 대기상태로 두고, // lock 은 뮤텍스를 락한것

     다른 스레드에서 _condition.notify_one(); or _condition.notify_all(); 로 wait 하고 있는 lock 을 깨울 수 있다

     

      


boost::mutex::scoped_lock lock(_mutex);

while(true){

_condition.wait(lock); //checkThread 함수 자체가 멈춘다

runThread();

}









http://www.boost.org/doc/libs/1_41_0/doc/html/thread/thread_management.html#thread.thread_management.threadgroup

Joining and detaching

When the boost::thread object that represents a thread of execution is destroyed the thread becomes detached. Once a thread is detached, it will continue executing until the invocation of the function or callable object supplied on construction has completed, or the program is terminated. A thread can also be detached by explicitly invokingthe detach() member function on the boost::thread object. In this case, the boost::thread object ceases to represent the now-detached thread, and instead representsNot-a-Thread.

In order to wait for a thread of execution to finish, the join() or timed_join() member functions of the boost::thread object must be used. join() will block the calling thread until the thread represented by the boost::thread object has completed. If the thread of execution represented by the boost::thread object has already completed, or the boost::thread object represents Not-a-Thread, then join() returns immediately. timed_join() is similar, except that a call to timed_join() will also return if the thread being waited for does not complete when the specified time has elapsed.



쓰래드가 





http://mytechdic.blogspot.kr/2010/03/boostthread-%EC%9D%98-sleep-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0.html


boost 라이브러리의 thread를 사용할 때 sleep을 사용할 일이 자주 생기는데

boost::thread::sleep 함수는 사용하기가 좀 번거롭습니다.

대신 boost::this_thread::sleep 함수를 사용하면 됩니다.

 

[code cpp]boost::this_thread::sleep(boost::posix_time::milliseconds(30));[/code]

 

인자로 사용되는 boost::posix_time::milliseconds 대신boost::posix_time::seconds 함수 등으로 시간 단위를 지정할 수 있습니다.






http://jeremyko.blogspot.kr/2012/03/boost-thread-13.html


boost::thread

OS간 포터블한 쓰레드 처리를 위해서 유용하다.
사용법은 다음과 같다. boost::thread 생성자에 쓰레드로 동작할 함수를 전달하면,
생성즉시 thread()가 동작한다.


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

void wait(int seconds)
{
   boost::this_thread::sleep(boost::posix_time::seconds(seconds));
}

void thread()
{
   for (int i = 0; i < 5; ++i)
   {
         wait(1);
         std::cout << i << std::endl;
   }
}

int main()
{
   boost::thread t(thread);
   t.join(); //thread()가 종료할때까지 blocking된다.
}

t변수를 이용해서 생성된 쓰레드에 접근할수 있다. 그런데, t변수가 스코프를
벗어나서 삭제되더라도 쓰레드 함수는 계속 동작한다.
쓰레드는 언제나 boost::thread 타입의 변수에 바인드되지만, 한번 생성되고 난 이후에는
그 변수에 의존적이지 않다. detach() 함수를 이용하면 이러한 boost::thread 타입
변수도 불필요하다.

위의 예제가 쓰레드종료를 기다리는 경우라면, 다음은 인터럽트되는 경우를 보여준다.

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

void wait(int seconds)
{
   boost::this_thread::sleep(boost::posix_time::seconds(seconds));
}

void thread()
{
   try
   {
         for (int i = 0; i < 5; ++i)
         {
       wait(1);
      
       std::cout <<"thread:"<< boost::this_thread::get_id()<<" "<< i << std::endl;
         }
   }
   catch (boost::thread_interrupted&)
   {
   std::cout << "thread interrupted" << std::endl;
   }
}

int main()
{
std::cout << boost::thread::hardware_concurrency() << std::endl; //동시에 실행가능한 쓰레드 갯수
    std::cout <<"(main)thread main id:" << boost::this_thread::get_id()<< std::endl;    
   boost::thread t(thread);
    std::cout <<"(main)thread id:" << t.get_id()<< std::endl;   
   wait(3);
   t.interrupt();
   t.join();
}

출력:
(main)thread main id:001696A8
(main)thread id:00169C70
thread:00169C70 0
thread:00169C70 1
thread:00169C70 2
thread interrupted

스레드 객체에 대해서 interrupt()를 호출하면, 그 쓰레드는 인터럽트된다.
이것은 해당 쓰레드에서 boost::thread_interrupted 예외를 발생 시키게 한다.
중요한점은, 이것은 쓰레드가 interruption point에 도달해야지만 발생된다는 것이다.
만약 쓰레드가 interruption point를 가지고 있지 않다면, interrupt()를 호출해도
아무것도 발생되지 않는다. 쓰레드는 interruption point에 도착할때마다,
interrupt()가 호출되었는지를 확인할것이고, 호출된경우 boost::thread_interrupted
예외가 전달될것이다.
Boost thread는 다음과 같은 interruption point를 정의하고 있다.

boost::thread::join()
boost::thread::timed_join()
boost::condition_variable::wait()
boost::condition_variable::timed_wait()
boost::condition_variable_any::wait()
boost::condition_variable_any::timed_wait()
boost::thread::sleep()
boost::this_thread::sleep()
boost::this_thread::interruption_point()

위의 예에서 sleep()은 5번 호출되고 있는데,
이경우에만 interrupt()호출 여부를 확인할것이고, 그외 시간에는 쓰레드는 인터럽트 되지 않는다.

boost::this_thread::disable_interruption
boost::this_thread::restore_interruption
쓰레드내에서 임시 객체를 선언하면 인터럽트되는것을 조정할수 있다.

void g()
{
    // interruption enabled here
    {
        boost::this_thread::disable_interruption di;
        // interruption disabled
        {
            boost::this_thread::restore_interruption ri(di);
            // interruption now enabled
        } // ri destroyed, interruption disable again
    } // di destroyed, interruption state restored
    // interruption now enabled
}


쓰레드 생성시, 함수에 입력인자 전달
void thread_func   (int i,double d,std::string s)
{
    std::cout << i<<":"<<d<<":"<<s<< std::endl;
}

int main()
{
    boost::thread t(thread_func, 42,3.141,"hello world");
    t.join();
}

인자는 모두 복사되어져서 전달되므로 , 참조자를 넘기는 경우는
boost::ref()를 이용한다.

int i;
void thread_func(int& j);
boost::thread t (thread_func,boost::ref(i));

Movable 
boost::thread 인스턴스간에는 소유권 이전이 가능하다.

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

void some_function()
{
    std::cout << "some_function..."<< std::endl;
}

boost::thread create_thread()
{    
    boost::thread t(some_function);

    return boost::move(t);
}

boost::thread threads[45];

int main()
{
    threads[12] = boost::move( create_thread() );
    threads[12].join();
}








http://cafe.naver.com/totalprogramming/14  에서 일부 발췌




쓰레드 함수에 파라메터를 넘겨줘야 하면 -> boost::bind를 쓰자

클래스 멤버 함수를 쓰레드로 돌릴 경우에도, bind를 써야 하고,

전역에 있는 함수를 쓰레드로 돌릴 경우는 bind를 사용하지 않아도 된다.

전역 변수이고 클래스 함수이고의 구분은 컴파일러가~알아서~한다




 



가변 인수를 받아들이는 thread함수. 생성자일 경우

Thread Constructor with arguments


void CKsc::start()

{

    thread_ = new boost::thread( boost::bind(&CKsc::f, this, 1, 2, this ));

}

생성과 동시에 thread 함수인 f가 돌기 시작한다.

두번째 매개 변수는 첫번째로 넘긴 CKsc::f의 대상 객체가 된다. 위의 예제에서는 CKsc객체 내에서 호출을 하기 때문에

this가 가능한것이다. 외부에서 할 경우에는

CKsc a;

    boost::thread* thread_ = new boost::thread( boost::bind(&CKsc::f, a, 1, 2, this ));

혹은

   boost::thread* thread_ = new boost::thread( boost::bind(&CKsc::f, &a, 1, 2, this ));

 

boost는 쓰레드 함수 지정시 이미 쓰레드는 자동으로 수행된다는 점이다

 

이렇게 하면 된다.

첫번째 처럼 하면 a가 복사되어 넘어가게 되는것이고, 두번째 예제처럼 하면 포인터가 넘어가서 공유가 된다.

 

 

void hello(const char* s)
...
int main() {
    thread t( bind(&hello, ",world") );

}




인수 없는 thread 함수를 받아들일 경우 생성자.

Thread Constructor


boost::thread ta(&show_trace);

ta.join();











http://cafe.naver.com/javacircle/12487



#include
 <iostream>
#include <boost/thread.hpp>
using namespace std;
using namespace boost;

void show_a() // 100만번 'a'를 출력
{
    for(int i=0; i!=1000000; ++i)
        cout << 'a';
}

void show_b() // 100만번 'b'를 출력
{
    for(int i=0; i!=1000000; ++i)
        cout << 'b';
}

int main()
{
    thread ta( &show_a ); // show_a 함수를 실행하는 스레드를 실행 
    thread tb( &show_b ); // show_b 함수를 실행하는 스레드를 실행 
    ta.join();
    tb.join(); // join() 함수로, 스레드가 종료될때까지 대기.
    return 0;
}

 

실행결과:

thread1

 

aaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbb

aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbb

aaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbb

....







http://www.viper.pe.kr/cgi-bin/moin.cgi/Boost/Thread_1




1. 참고링크 [Bottom] [Top]

2. boost::thread 라이브러리 [Bottom] [Top]

<!> boost::thread 라이브러리에 대한 설명

3. <boost/thread/thread.hpp> 헤더 파일 [Bottom] [Top]

3.1. thread 클래스 [Bottom] [Top]

  • Thread 객체를 생성하는 클래스로 boost::thread 라이브러리 의 (!) 핵심 Thread 클래스.

  • 생성자
    Toggle line numbers
    thread( const boost::function0< void >& threadfunc );
    
    • 쓰레드 객체를 생성한다.
    • 생성자의 매개변수는 반환값이 void 형이고 매개변수가 없는 함수 또는 함수 객체를 전달한다. 자세한 것은 functionN<> 클래스를 참고한다.

  • 멤버 함수
    • join() 함수 - 쓰레드가 종료될 때까지 대기한다.
    • sleep( xt ) 함수 - xt 로 지정된 시간 만큼 쓰레드를 대기한다.
    • yield() 함수 - 쓰레드를 Ready 상태로 전환하고 Context Switching 한다.

3.2. thread_group 클래스 [Bottom] [Top]

  • Thread 객체를 생성하고 관리해주는 Thread Manager.

  • 멤버 함수
    • create_thread( threadfunc ) 함수 - 쓰레드를 생성한 후 쓰레드 관리 컨테이너에 추가한다.
    • add_thread( thrd ) 함수 - 생성된 쓰레드를 쓰레드 관리 컨테이너에 추가한다.
    • remove_thread( thrd ) 함수 - 지정된 쓰레드를 쓰레드 관리 컨테이너에서 삭제한다.
    • join_all() 함수 - 모든 쓰레드가 종료될 때까지 대기한다.
    • size() 함수 - 현재 쓰레드 관리 컨테이너의 크기 (쓰레드 개수) 를 구한다.











http://blog.naver.com/pronaman/60125230225



Boost.Thread는 데이터 공유의 실행 스레드를 사용 가능하게 하고, 스레드를 이동시킬 수 있으며 스레드간에 데이터를 동기화하는 함수를 제공한다. 

#include <boost/thread.hpp>

 

사용방식은 다음과 같다. 
- 스레드함수가 외부에 있을때 : boost::thread myThread( &func );
- 스레드함수가 클래스의 멤버 함수일때 : boost::thread myThread( boost::bind(&ClassName::func, this) ); 

간단한 사용법은 다음과 같다.

#include "boost/thread.hpp"
#include <iostream>
#include <time.h>

void ThFunc(void *p, int a, int b, int c, int d)
{
    const char *pszMessage = (const char *)p;
    std::cout << pszMessage << ":" << (a+b+c+d) << std::endl;
    boost::this_thread::sleep(boost::posix_time::seconds(2));          // 2초 동안 정지
}
  
int main(int argc, char **argv)
{
    static const char *pszMessage = "boost thread";
    boost::thread t(ThFunc, (void *)pszMessage, 1102420484096);  // 여러개의 인자를 쓰레드 함수에 전달 할 수 있음.
  
    t.timed_join(boost::posix_time::seconds(1));                       // 1초 동안 쓰레드 종료 대기
    t.join();                                                          // 쓰레드 종료 대기 (무한)
      
    return 0;
}

여러개의 스레드간의 임계영역 처리시 호환성때문에 윈도우의 크리티컬 섹션과 같은 클래스는 없는것 같다. 뮤텍스로 처리한다.

사용법은 다음과 같다.

 

boost::mutex mutexVar; // Lock을 걸 mutex 변수

/////////////////////////////////////////
스레드 합수 안에서 스코프를 만들어 락을 처리하는 것이 좋다.

{
     boost::mutex::scoped_lock( mutexVar ); // Mutex Lock이 걸림
}

 

스레드를 실행 후 정지시켰다가 특정 시점에 활성화 하고자 할 경우 윈도에서는 이벤트 객체를 사용했으나 boost는

condition 이란 객체를 사용한다. 컨디션은 쓰레드를 잠들게 하고 동작하게 하는 제어 변수로  사용될 수 있다.

condition.wait(mutexVar);

 

스레드에서 이와 같이 선언한 경우 스레드가 멈추게 된다. 이때 다음 함수로 활성화할 수 있다.

 

condition.notify_one(); // 잠들어있는 것중에서 경쟁적으로 한개만 깨울때
condition.notify_all(); // 잠들어있는 모든 녀석을 깨울때

[출처] thread|작성자 마니 조우성






Advanced Usage

----------------------------------------------------------------------------------
  • Thread를 제어하는 2가지: Mutex 와 condition

    • Mutex : 운영체제에서 익히나온 것이다. Thread 가 동시다발적으로 어느 특정 리소스를 사용할때, 한개의 Thread만 사용하게 하고싶을때 사용하는 녀석이다. 제대로 사용하지 않으면 성능저하를 일으키니 디자인설계때부터 조심히 사용해야 하는 녀석이다. 추천하는

      - header : #include <boost/thread/mutex.hpp>
      - 추천하는 것 : boost::mutex::scoped_lock
      - 이유 : 사용하기 편하다. 이 변수를 괄호 안에 선언하게 되면 괄호의 끝을 만날때까지 잠금이 되고, 괄호를 벗어나게되면 락이 풀린다. (이에 대한 원리는 간단히 생각해보면 되는데, 생성자에서 락을 걸고, 소멸자에서 락을 푸는 코드를 삽입한 것이다)

      boost::mutex mutexVar; // Lock을 걸 mutex 변수

      if ( test_module == true )
      {
           boost::mutex::scoped_lock( mutexVar ); // Mutex Lock이 걸림
           // Do something
      } // 이 시점에서 락이 풀린다.


    • condition : 쓰레드를 잠들게 하고 동작하게 하는 제어 변수. 보통 어떤 값을 기다릴때 while 로 돌리는 방법이나 sleep 을 줘서 일정시간마다 체크하는 경우가 있는데 이는 대부분 비효율적이며, 데이터가 아직 완성이 안되어있으면 잠들어있다가 완료시점에 신호를 보내어 깨어나게 한다. 기억해야 할 점은, 만약 집단의 쓰레드들을 관리해서 경쟁적으로 한개씩 깨어나게 하고 싶다면 하나의 condition 변수를 쓰레드들 사이에서 공유해야 한다.

      void threadFunction(void)
      {
           while( true )
           {
                boost::mutex::scoped_lock lock( mutexVar );
                // Thread 초기화 작업
                condition.wait(mutexVar); // 현재 실행중인 Thread는 대기모드로
                                                    // 들어가고 mutexVar로 걸린 락이 해지
                                                    // 된다.
      // 외부 코드에서 깨우고 싶을때.. 
      condition.notify_one(); // 잠들어있는 것중에서 경쟁적으로 한개만 깨울때
      condition.notify_all(); // 잠들어있는 모든 녀석을 깨울때






The Boost.Threads Library


http://diehard98.tistory.com/141




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

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

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

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

Thread Creation

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

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

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

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

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

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







 

Mutexes

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

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

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

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

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

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

boost::mutex io_mutex;

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

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

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

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


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


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

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

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

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

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

boost::mutex io_mutex;

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


Condition Variables

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

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

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


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

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

boost::mutex io_mutex;

class buffer{ public: typedef boost::mutex::scoped_lock scoped_lock; buffer(): p(0), c(0), full(0){ }

void put(int m){    scoped_lock lock(mutex);     if (full == BUF_SIZE)     {         {             boost::mutex::scoped_lock lock(io_mutex); 

           std::cout <<"Buffer is full. Waiting..."<< std::endl;         }         while (full == BUF_SIZE)             cond.wait(lock);     }

buf[p] = m; p = (p+1) % BUF_SIZE; ++full; cond.notify_one();

}

int get(){     scoped_lock lk(mutex);     if (full == 0){         {             boost::mutex::scoped_lock lock(io_mutex);

           std::cout <<"Buffer is empty. Waiting..."<< std::endl;         }         while (full == 0)             cond.wait(lk);     }

int i = buf[c]; c = (c+1) % BUF_SIZE; --full; cond.notify_one(); return i;

} private:     boost::mutex mutex;     boost::condition cond;     unsigned int p, c, full;     int buf[BUF_SIZE]; };

buffer buf;

void writer(){     for (int n = 0; n < ITERS; ++n){         {             boost::mutex::scoped_lock lock(io_mutex);             std::cout << "sending: "<< n << std::endl;         }

       buf.put(n);     } }

void reader(){     for (int x = 0; x < ITERS; ++x){         int n = buf.get();         {         boost::mutex::scoped_lock lock(io_mutex);         std::cout << "received: " << n << std::endl;         }     } }

int main(int argc, char* argv[]) {     boost::thread thrd1(&reader);     boost::thread thrd2(&writer);     thrd1.join();     thrd2.join(); return 0; 

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

Thread Local Storage

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

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

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

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

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

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

struct count{

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

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

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

}


int id;

};

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

}

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

Once Routines

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

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

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

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

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

void init() {     ++i; }

void thread1() {     boost::call_once(&init, flag); }

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

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

The Future of Boost.Threads

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

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

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

Notes

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

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

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

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


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

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

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

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


반응형

'메타프로그래밍 > Boost::' 카테고리의 다른 글

boost::random  (0) 2013.01.29
Boost::mpl  (0) 2012.12.25
boost::weak_ptr  (0) 2012.11.24
boost::shared_ptr  (0) 2012.11.18
boost::function  (0) 2012.11.13

+ Recent posts