unique_lock는 쉽게 말하면 lock_guard에 기능이 더해진 것이라고 볼 수 있습니다.

lock_guard의 경우 정의와 동시에 락을 걸고, 파괴될 때만 락을 풀 수 있습니다.

 

그에 비해 unique_lock은 정의와 동시에 락을 걸 수도 있고, 또는 뒤에 락을 걸 수도 있습니다. 또 락을 푸는 것도 원하는 대로 조절이 가능하며 try_lock이나 try_lock_for, try_lock_until 등도 지원합니다. 그리고 unique_lock끼리 소유한 뮤텍스 객체를 서로 교환도 할 수 있으며, 뮤텍스 객체를 얻거나 해제할 수도 있습니다.

 

간단한 경우에는 lock_guard가 사용하기 편하지만 뮤텍스를 다양하게 사용하고 싶은 경우에는 unique_lock이 훨씬 더 좋습니다.

 

 

unique_lock 생성

unique_lock 클래스의 생성자는 다양한 방법으로 뮤텍스를 사용할 수 있도록 여러 버전이 준비 되어 있습니다.

 

unique_lock() noexcept;

unique_lock(unique_lock&& Other) noexcept;

explicit unique_lock(mutex_type& Mtx);

unique_lock(mutex_type& Mtx, adopt_lock_t Adopt);

unique_lock(mutex_type& Mtx, defer_lock_t Defer) noexcept;

unique_lock(mutex_type& Mtx, try_to_lock_t Try);

template<class Rep, class Period>

   unique_lock(mutex_type& Mtx,

      const chrono::duration<Rep, Period> Rel_time);

template<class Clock, class Duration>

   unique_lock(mutex_type& Mtx,

      const chrono::time_point<Clock, Duration> Abs_time);

unique_lock(mutex_type& Mtx,

   const xtime *Abs_time) noexcept;

 

unique_lock의 사용 예는 아래와 같습니다.

 

std::mutex m;

....

// 락을 만들고 동시에 뮤텍스의 소유권을 가진다

std::unique_lock lck(m, std::adopt_lock);

 

 

std::mutex m;

....

// 락을 만들지만 뮤텍스의 소유권은 가지지 않는다

std::unique_lock lck(m,std::defer_lock);      

...

if (lck.try_lock()) {

....

}                   

 

 

std::unique_lock<std::timed_mutex>

        lk(m,std::chrono::milliseconds(3));

....

if(lk) {

    process(data);

}                   

 

 

 

unique_lock 멤버 함수

 

- void lock();

- mutex_type *mutex() const _NOEXCEPT;

- explicit operator bool() _NOEXCEPT

- unique_lock& operator=(

    unique_lock&& Other

  ) _NOEXCEPT;

- bool owns_lock() const _NOEXCEPT;

- mutex_type *release() _NOEXCEPT;

- void swap(

    unique_lock& Other

  ) _NOEXCEPT;

- bool try_lock() _NOEXCEPT;

- template<class Rep,

    class Period>

  bool try_lock_for(

     const chrono::duration<Rep,

     Period>& Rel_time

  );

- template<class Clock, class Duration>

    bool try_lock_until(const chrono::time_point<Clock, Duration>& Abs_time);

    bool try_lock_until(const xtime *Abs_time);

- void unlock();

 

저작자 표시
신고
by 흥배 2013.07.15 08:00

std::mutex try_lock을 사용하면 뮤텍스의 소유권을 가질 수 있으면 true를 반환하고, 그렇지 못하면 바로 false를 반환하므로 소유권을 얻고 싶으면 또 다시 try_lock을 호출해야 합니다. 그런데 공유자원을 사용하는 시간이 아주 짧은 경우도 있다면 특정 시간까지만 계속 락을 시도하여 뮤텍스의 소유권을 가지기를 바라는 경우가 있습니다. 이럴 때 사용하는 것이 timed_mutex입니다.

 

timed_mutex std::mutex의 모든 기능을 다 가지고 있으면서 try_lock_for try_lock_until 함수 두 개가 더 있습니다. 이 함수들은 시간을 사용하여 뮤텍스 소유를 시도합니다. 시간은 std::chrono로 설정합니다.

 

try_lock_for는 지정한 시간 동안 락을 걸어봅니다.

template<class Rep, class Period>

   bool try_lock_for(const chrono::duration<Rep, Period>& Rel_time);

 

// 10초 동안 mutex의 소유권을 얻을 때까지 시도

if( mutex.try_lock_for(std::chrono::seconds(10)) )

{

           // 공유 자원 사용

           mutex.unlock();

}

 

 

try_lock_until은 지정한 시간까지 뮤텍스의 소유를 시도합니다.

 

template<class Clock, class Duration>

   bool try_lock_for(const chrono::time_point<Clock, Duration>& Abs_time);

bool try_lock_until(const xtime *Abs_time);

 

std::chrono::system_clock::time_point CurTime = std::chrono::system_clock::now();

std::chrono::seconds dura_sec( 5 );

 

// 현재 시간에서 5초 후까지만 mutex를 소유하려고 시도한다

if( mutex.try_lock_until(CurTime + dura_sec) )

{

           // 공유 자원 사용

           mutex.unlock();

}


 

 

recursive_timed_mutex

recursive_timed_mutex std::mutex에 시간 기능이 추가된 timed_mutex 처럼 recursive_mutex에 시간 기능이 추가된 것입니다.

recursive_mutex에 추가된 함수나 사용 방법은 timed_mutex와 비슷합니다.

 

 

저작자 표시
신고
by 흥배 2013.07.08 08:00

std::mutex lock 멤버 함수의 설명을 보면 lock을 호출한 함수에서 unlock을 호출하지 않은 상태에서 또 다시 lock을 호출하면 알 수 없는 동작을 한다고 되어 있습니다.

 

스레드에서 lock을 호출한 후 다시 lock을 호출하는 경우는 아래와 같은 경우입니다.

 

class buffer {

    list<int> queue;

    std::mutex mut;

public:

    bool empty() {

        std::lock_guard<std::mutex> lock(mut);

        return queue.empty();

    }

    ...

    int pop() throw(out_of_range) {

        std::lock_guard <std::mutex> lock(mut);

        while (empty())

        {   

……..

        }

        int tmp = queue.front();

        queue.pop_front();

        return tmp;

     }

};

코드 출처 : http://d.hatena.ne.jp/hidemon/20081218/1229555003

 


위의 buffer 클래스를 보면 pop() 함수를 호출하면 먼저 mutex로 락을 건 다음 empty() 함수를 호출하는데 empty도 같은 mutex를 사용하여 lock을 걸고 있습니다. 즉 이미 락을 건 mutex에 또 lock을 걸고 있습니다. 이런 경우 데드락 상황에 빠질 수가 있습니다. 이 문제를 풀기 위해서는 recursive_mutex를 사용합니다.

recursive_mutex는 같은 스레드에서 lock을 건 후 또 다시 lock을 걸어도 괜찮습니다. lock을 건 횟수만큼 꼭 unlock을 호출해야 합니다.

 

그럼 아래는 위의 buffer 클래스를 recursive_mutex를 사용하여 올바르게 수정한 것 입니다. 단순하게 std::mutex std::recursive_mutex로 바꾸면 됩니다.

(std::mutex std::recursive_mutex의 사용 방법은 같습니다)

 

class buffer {

    list<int> queue;

    std::recursive_mutex mut;

public:

    bool empty() {

        std::lock_guard<std::recursive_mutex> lock(mut);

        return queue.empty();

    }

    ...

    int pop() throw(out_of_range) {

        std::lock_guard <std::recursive_mutex> lock(mut);

        while (empty())

        {   

……..

        }

        int tmp = queue.front();

        queue.pop_front();

        return tmp;

     }

};

 

 

저작자 표시
신고
by 흥배 2013.07.01 08:00

std::mutex를 사용하여 복수의 스레드에서 사용하는 공유 자원을 서로 침범 없이 사용할 수 있습니다. 그러나 lock 사용하여 뮤텍스에 대한 소유권을 얻은 후 실수로 unlock을 하지 않으면 아무도 그 공유 자원을 사용하지 못하는 데드락 상태에 빠질 수 있습니다.

 

예를 들면 아래와 같은 경우가 이런 실수를 하기 쉽습니다.

..........

mutex.lock();

 

if( n < 10 ) {

return false;

}

.......

mutex.unlock();

 

위 코드에서 n 10보다 작은 경우 unlock을 하지 않고 바로 빠져 나오게 됩니다. 이거 이외에도 lock을 사용한 후 예외가 발생하여 throw 되는 경우 unlock을 하지 않는 실수를 할 수 있습니다.

 

이런 실수를 방지하기 위해서 lock_guard 라는 유틸리티 클래스가 있습니다.

mutex lock_guard와 같이 사용하면 lock_guard 클래스가 인스턴스화 될 때 자동으로 lock을 호출하고, 인스턴스가 파괴될 때 자동으로 unlock을 호출합니다.

 

template<class Mutex>

class lock_guard;

 

다음은 lock_guard를 사용한 예입니다.

 

< 예제. 2 >

#include <thread>

#include <iostream>

#include <mutex>

 

int main()

{

           std::mutex mtx_lock;

 

           std::thread Threads1( [&] ()

              {

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

                        {

                                          std::lock_guard<std::mutex> guard(mtx_lock);

                                          std::cout << "Thread Num : " << i << std::endl;

                        }

              } );

 

 

           std::thread Threads2;

           Threads2 = std::thread( [&] ()

              {

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

                        {

                                          std::lock_guard<std::mutex> guard(mtx_lock);

                                          std::cout << "Thread Num : " << i << std::endl;

                        }

              } );

 

           std::thread Threads3 = std::thread( [&] ( int nParam )

              {

                      for( int i = 20; i < 25; ++i )

                      {

                                 std::lock_guard<std::mutex> guard(mtx_lock);

                                 std::cout << "Thread Parameter : " << nParam << std::endl;

                      }

              }, 4 );

 

          

 

           Threads1.join();

           Threads2.join();

           Threads3.join();

          

           return 0;

}

 

lock_guard는 뮤텍스의 소유와 해제에 대한 RAII(리소스 확보 및 초기화) 패턴이라고 볼 수 있습니다.

저작자 표시
신고
by 흥배 2013.06.24 08:00

병렬 프로그래밍을 해보면 싫든 좋든 여러 스레드에서 하나의 객체에 동시에 접근하는 경우가 발생합니다. 앞선 std::thread에 관한 글을 보면 소개한 예제 중 그런 경우를 보실 수 있을 겁니다.

 

< 예제 >

#include <thread>

#include <iostream>

 

int main()

{

           std::thread Threads1( [] ()

              {

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

                        {

                                          std::cout << "Thread Num : " << i << std::endl;

                        }

              } );

 

 

           std::thread Threads2;

           Threads2 = std::thread( [] ()

              {

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

                        {

                                          std::cout << "Thread Num : " << i << std::endl;

                        }

              } );

 

           std::thread Threads3 = std::thread( [] ( int nParam )

              {

                      for( int i = 20; i < 25; ++i )

                      {

                                std::cout << "Thread Parameter : " << nParam << std::endl;

                      }

              }, 4 );

 

           getchar();

           return 0;

}

 

위 예제에서 ‘std::cout’ 각 스레드에 동시에 접근하고 있습니다. 그래서 실행하면 아래와 같은 결
과가 나옵니다.


실행 화면을 보면 출력 결과 중 일부가 뒤죽박죽 되어 있는 것을 알 수 있습니다. 이유는 Thread1 스레드에서 출력스트림 결과를 다 출력하기 전에 Threads2가 출력스트림을 사용하여 발생한 문제입니다. 이런 것을 data-race(데이터 경합) 이라고 합니다.

 

그런데 위 예제를 여러 번 실행해 보면 아래처럼 올바르게 나올 때도 있습니다.



사실 이런 부분이 병렬 프로그래밍의 어려움 중의 하나입니다. 여러 스레드에서 하나의 공유 자원을 사용할 때 타이밍에 의해서 순서대로 사용할 수도 있고 그렇지 못할 수도 있기 때문에 버그 발생이 언제 어떻게 생길지 알기 힘듭니다.

 

병렬 프로그래밍을 할 때는 언제나 공유 자원을 최소한으로 하고, 공유 자원은 꼭 동기화 객체를 사용하여 여러 스레드가 동시에 사용하지 못하도록 해야 합니다.

 

위의 문제는 동기화 객체를 사용하면 해결 할 수 있습니다. 여기서는 앞선 글에서 사용한 적이 있는 뮤텍스(std::mutex)를 사용하겠습니다.

 

그럼 위 예제를 mutex를 사용하여 문제를 해결해 보겠습니다.

 

<예제 1 >

#include <thread>

#include <iostream>

#include <mutex>

 

int main()

{

           std::mutex mtx_lock;

 

           std::thread Threads1( [&] ()

              {

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

                        {

                                          mtx_lock.lock();

                                          std::cout << "Thread Num : " << i << std::endl;

                                          mtx_lock.unlock();

                        }

              } );

 

 

           std::thread Threads2;

           Threads2 = std::thread( [&] ()

              {

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

                        {

                                          mtx_lock.lock();

                                          std::cout << "Thread Num : " << i << std::endl;

                                          mtx_lock.unlock();

                        }

              } );

 

           std::thread Threads3 = std::thread( [&] ( int nParam )

              {

                      for( int i = 20; i < 25; ++i )

                      {

                                 mtx_lock.lock();

                                 std::cout << "Thread Parameter : " << nParam << std::endl;

                                 mtx_lock.unlock();

                      }

              }, 4 );

 

          

 

           getchar();

           return 0;

}

 

< 실행 결과>



std::mutex를 사용하기 위해서는 아래의 헤더 파일을 추가합니다.

#include <mutex>

 

스레드에서 공유 자원을 사용할 때는 아래 처럼 락을 걸어서 뮤텍스 mtx_lock의 소유권을 얻어서 다른 스레드가 접근하지 못하도록 합니다. 그리고 공유 자원을 다 사용하였다면 락을 해제하여 mtx_lock의 소유권을 버립니다.

mtx_lock.lock();

std::cout << "Thread Num : " << i << std::endl;

mtx_lock.unlock();

 

락을 건 후(lock) 락을 풀(unlock) 때까지는 다른 스레드는 대기를 하고, 락을 풀면 대기하고 있는 스레드 중 하나가 락을 건 후 공유 자원을 사용합니다.

그러므로 락을 건 후에는 꼭 락을 풀어야 하고(만약 풀지 않으면 데드락 상황에 빠지게 됩니다), 락을 사용하는 부위가 너무 빈번하면(스레드 대기가 늘어나므로) 병렬 프로그래밍의 장점이 많이 약해집니다.

 

 

try_lock

std::mutex lock unlock 이외에 try_lock 이라는 멤버 함수가 있습니다.

bool try_lock();

 

스레드에서 lock을 호출할 때 다른 스레드가 이미 lock을 호출하여 뮤텍스의 소유권을 가진 후 아직 unlock을 호출하지 않았다면 그 자리에서 대기를 합니다. 그러나 try_lock은 대기를 하지 않고 false를 반환합니다(뮤텍스의 소유권을 얻지 못한 경우). 반대로 true를 반환한 경우는 뮤텍스에 대한 소유권을 가지게 되므로 공유자원을 안전하게 사용할 수 있습니다.

 

try_lock을 사용하는 경우는 만약 공유 자원을 다른 스레드에서 사용 중이면 스레드가 대기하면서 그냥 시간을 소비하지 않고 또 다른 작업을 하도록 할 때 사용하면 좋습니다.

 


 

ps : 참고로 std::mutex Windows OS에서는 내부적으로는 크리티컬섹션을 사용합니다.

 

 

저작자 표시
신고
by 흥배 2013.06.03 08:00

앞 선 글 중 ‘std::thread - 2. Join’에서 join을 호출하지 않은 경우 에러 창이 발생하였습니다.


 

이것은 C++11 thread 사양에 의해서 thread 오브젝트가 파괴될 때 join이나 detach를 호출하지 않은 경우 std::terminate를 호출하여 프로그램을 종료하도록 되어 있기 때문입니다.

 

< 예제. 9 >

#include <thread>

#include <iostream>

 

int main()

{

           int nThreadRunCount = 0;

          

           {

                      std::thread Thread = std::thread( [&] ()

                       {

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

                          {

                                ++nThreadRunCount;

                                                               

                                std::cout << "Thread1 ID : " << Thread.get_id() << std::endl;

                          }

                       } );

           }

 

           getchar();

           return 0;

}

 


<예제.9>를 실행하면 에러가 발생하고 아래의 콜스택을 보면



thread 오브젝트의 소멸자에서 terminate를 호출하는 것을 알 수 있습니다.



이것으로 thread에 대해서 기능 위주로 간단하게 살펴 보았습니다. 앞으로 공부해야 할 것들이 더 있어서 남은 것들을 설명 후 다시 thread 사용에 대해서 깊게 들어 가겠습니다.

 

다음부터는 병렬 프로그래밍에서 절대 빠질 수 없는 동기화 객체에 대해서 설명하겠습니다.

 

 

저작자 표시
신고
by 흥배 2013.05.28 08:00

hardware_concurrency()는 하드웨어 스레드 컨텍스트 수를 알려 주는 함수입니다.

스레드 컨텍스트는 CPU , 코어 수, 하이퍼 스레드와 관계가 있습니다.

 

하드웨어 스레드 컨텍스트 수는 Windows '작업 관리자' '성능' 탭에서 볼 수 있습니다.





제 컴퓨터는 CPU 하나에 코어는 4개이고, 하이퍼 스레딩을 지원하지 않는 CPU라서 CPU 사용 현황에 4개의 그림만 나옵니다. 그래서 저는 hardware_concurrency() 함수를 실행하면 결과는 4가 나옵니다.

 

관례적으로 프로그램에서 멀티 스레드를 만들 때 (하드웨어 스레드 컨텍스트 수 * 2) +1 이라는 공식으로 나온 수 만큼 만들면 좋다고 합니다. 이때 hardware_concurrency() 함수를 사용하면 좋습니다.

그리고 만약 hardware_concurrency()를 사용할 수 없는 경우라면 0 을 반환 합니다.

 

< 예제. 7 >

#include <thread>

#include <iostream>

 

 

int main()

{

           std::cout << "하드웨어 컨텍스트 수 : " << std::thread::hardware_concurrency() << std::endl;

 

           return 0;

}

 

< 실행 결과 >


저작자 표시
신고
by 흥배 2013.05.10 08:00

C++11의 새로운 기능 중 'SCARY iterator' 라는 것이 있습니다. 단순히 한국말로 바꾸면 '무서운 반복자' 라고 할 수 있는데 사실 무섭지는 않습니다.^^;

 

SCARY iterator에 대해서 아주 간단하게 말하면 컨테이너를 선언할 때 템플릿에 들어가는 데이터 타입만 같다면 비교 함수나 메모리 할당자가 서로 달라도 같은 반복자를 사용할 수 있게 해줍니다(만약 메모리 할당자의 차이에 의해서 이상한 결과가 나올 수도 있어서 무서운 이라는 단어를 사용하지 않았나 생각합니다);

 

SCARY iterator가 구현 되기 전에는 아래와 같이 데이터 타입은 같으나 비교 함수가 서로 다른


std::set< int, std::less<int> > map1;

std::set< int, std::greater<int> > map2;


에서 map1 map2의 반복자는 서로 같지 않습니다. 분명히 int라는 같은 타입을 사용해서 같은 반복자를 사용해도 문제 없

을 거라고 생각되지만 비교 함수가 서로 틀려서 반복자는 서로 다른 타입으로 취급합니다. 그래서 아래의 코드는 컴파일 에러가 발생합니다.

 

std::set<int, std::less<int>>::iterator Iter = b.begin();

 

그러나 C++11SCARY iterator가 구현된 VC11에서는 위의 코드는 전혀 에러가 발생하지 않습니다.

 

VC11처럼 SCARY iterator가 구현된 컴파일러에서는 컨테이너의 데이터 타입만 같다면 같은 반복자를 사용할 수 있게 되었습니다.

 

 


 

참고 : http://blogs.wankuma.com/episteme/archive/2013/01/11/311581.aspx

 

저작자 표시
신고
by 흥배 2013.03.21 08:00

VS 2005를 사용하여 만든 프로젝트를 VS2012로 마이그레이션 하는 도중 템플릿 사용 부분에서 에러가 발생했습니다.

 

제가 겪은 에러가 이미 다른 사람들도 경험을 했고 MSConnect 사이트에 해결 책이 있어서 공유합니다.

 

struct default_policy

{

    template<class T>

    struct apply

    {

        typedef T type;

    };

};

 

template<class Policy=default_policy>

struct Def

{

    typedef Policy policy;

    typedef Def<policy> thisClass;

 

    typedef typename policy::apply<thisClass>::type type; // syntax error '<'

};

 

void main()

{

}

위 코드에서 typedef typename policy::apply<thisClass>::type type; 부분에서 에러가 발생합니다.

 

이것은 VC11의 문제가 아니고 위 코드가 표준을 지키지 않아서 발생한 문제입니다.

 

해결책은 아래와 같습니다(간단합니다)

struct default_policy

{

    template<class T>

    struct apply

    {

        typedef T type;

    };

};

template<class Policy=default_policy>

struct Def

{

    typedef Policy policy;

    typedef Def<policy> thisClass;

    typedef typename policy::template apply<thisClass>::type type;

};

 

 

typedef typename policy::apply<thisClass>::type type;

이것을 아래와 같이 바꾸면 됩니다.

typedef typename policy::template apply<thisClass>::type type;

 

 

출처 : http://connect.microsoft.com/VisualStudio/feedback/details/745629/typename-template-bug-in-c-compiler-in-visual-studio-2012-rc

 

 

저작자 표시
신고
by 흥배 2012.11.27 08:00

앞선 글의 <예제.1>을 보면 제일 아래에

 

#include <thread>

#include <iostream>

 

int main()

{

std::thread Thread1( [] ()

......

 

getchar();

return 0;

}

 

마지막 줄에서 두 번째에 getchar();를 사용했습니다. 만약 이것을 제거하고 예제 코드를 실행하면 다음과 같은 에러가 발생합니다.



이 에러가 발생하는 이유는 아직 스레드는 실행 중인데 프로그램이 종료 되었기 때문입니다. getchar();를 사용하여 유저의 입력을 받을 때까지 대기하도록 하여 문제를 해결할 수 있지만 이것은 임시 방편입니다. 올바른 방법은 thread 클래스의 join 함수를 사용하여 스레드의 실행이 끝날 때까지 대기하도록 합니다.

<예제.1> join을 사용하도록 수정하면 아래와 같습니다.

 

< 예제. 2 >

#include <thread>

#include <iostream>

 

int main()

{

std::thread Thread1( [] ()

           {

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

              {

                     std::cout << "Thread Num : " << i << std::endl;

              }

           } );

 

 

           std::thread Thread2;

           Threads = std::thread( [] ()

           {

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

              {

                 std::cout << "Thread Num : " << i << std::endl;

              }

           } );

 

           std::thread Thread3 = std::thread( [] ( int nParam )

                     {

                       for( int i = 20; i < 25; ++i )

                       {

                         std::cout << "Thread Parameter : " << nParam << std::endl;

                       }

                      }, 4 );

 

          

 

           Thread1.join();

Thread2.join();

Thread3.join();

          

return 0;

}

 

<예제.2>는 제일 아래에서 각 thread 객체의 join을 호출하고 있어서 Thread1, Thread2, Thread3는 생성과 동시에 실행이 되는데 만약 Thread1의 실행이 끝난 후 Thread2가 실행되고 이것이 끝나면 Thread3를 실행하려면 thread 객체를 생성 후 join을 호출하면 됩니다.

 

< 예제. 3 >

#include <thread>

#include <iostream>

 

int main()

{

std::thread Thread1( [] ()

           {

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

              {

                     std::cout << "Thread Num : " << i << std::endl;

              }

           } );

           Thread1.join();

 

 

           std::thread Thread2;

           Threads = std::thread( [] ()

           {

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

              {

                 std::cout << "Thread Num : " << i << std::endl;

              }

           } );

           Thread2.join();

 

 

           std::thread Thread3 = std::thread( [] ( int nParam )

                     {

                       for( int i = 20; i < 25; ++i )

                       {

                         std::cout << "Thread Parameter : " << nParam << std::endl;

                       }

                      }, 4 );

Thread3.join();

          

return 0;

}

 

< 결과 >




저작자 표시
신고
by 흥배 2012.11.19 08:30
| 1 2 3 |

티스토리 툴바