고수들이 절대 가르쳐 주지 않는 C/C++ 프로그래밍 팁 #3 A/S - C++ Thread 라이브러리

Posted at 2007. 7. 2. 07:00 // in S/W개발/고절가주팁 // by 김윤수


고절가주팁 세번째글에 대한 A/S 글입니다. 정확히 말하면 A/S 라기 보다는 이전 글이 너무 길어져서 둘로 나눈 거랍니다. 이번 글에서는 C++ 용 thread 클래스 라이브러리-Java Thread 라이브러리 참조-를 핵심 아이디어만 작성해 보도록 하겠습니다.

일단 기본적인 아이디어는 지난 글에서와 동일합니다. pthread 라이브러리에서 필요로 하는 callback 루틴으로 호출될 수 있는 별도의 Wrapping 함수나 클래스 메소드에 인자로 객체 인스턴스를 제공해 주고, 그 함수나 메소드 안에서 그 객체 인스턴스에 대해 일반 메소드를 호출하는 방식을 이용하면 됩니다.

C++ thread 클래스 라이브러리 인터페이스는 Java Thread 라이브러리를 모방해 보도록 하겠습니다. 제 생각으로는 Java Thread 라이브러리 인터페이스는 상당히 잘 설계된 인터페이스라고 생각됩니다.

본격적으로 C++ 용 thread 클래스 라이브러리를 작성해 보기 전에 Java Thread 라이브러리에 대해 아주 아주 간략하게 알아보도록 하겠습니다. Java Thread 라이브러리 인터페이스의 핵심은 Thread 라는 클래스와 Runnable 라는 인터페이스라고 할 수 있습니다. 그리고, Thread 는 Runnable 인터페이스-Runnable 인터페이스는 run()이라고 하는 딱 하나의 메소드-를 구현하고 있습니다. Thread 는 이와 더불어 상당히 세밀한 제어를 할 수 있도록 다양한 메소드를 정의하고 있습니다.

Java Thread 라이브러리를 사용하는 방법은 기본적으로 두 가지가 있습니다. 아래에 제시된 두가지 코드는 Java 코드입니다.

첫번째로 Thread 로부터 상속받아서 run() 메소드를 구현하는 방법이 있습니다. 예를 들어, 다음과 같이 하시면 됩니다.

// 확장된 Thread 클래스 정의
class PrimeThread extends Thread {
  long minPrime;
  PrimeThread(long minPrime) {
    this.minPrime = minPrime;
  }

  // run() 구현
  public void run() {
    // compute primes larger than minPrime
    . . .
  }
};

// 확장된 Thread 클래스 인스턴스 생성 후, start() 메소드 호출
PrimeThread p = new PrimeThread(143);
p.start();


두번째로 Runnable 인터페이스를 구현하는 구체 클래스의 인스턴스를 생성하고, 그 인스턴스를 Thread(Runnable) 생성자의 인자로 넣어 주면서 Thread 인스턴스를 생성하는 방법입니다. 첫번째와 같은 예를 이 방법으로 구현하려면 다음과 같이 하시면 됩니다.

// Runnable 인터페이스를 구현하는 구체 클래스
class PrimeRun implements Runnable {
  long minPrime;
  PrimeRun(long minPrime) {
    this.minPrime = minPrime;
  }

  // run() 구현
  public void run() {
    // compute primes larger than minPrime
    . . .
  }
}

// 생성된 Runnable 구현 구체 클래스 인스턴스를 Thread 인스턴스의 인자로 줌
PrimeRun p = new PrimeRun(143);
new Thread(p).start();

완벽한 C++ thread 클래스 라이브러리를 구현하는 것은 이 글의 범위를 훨씬 뛰어넘는 일이므로 이 글에서는 위의 두 가지 예에서 제시된 개념을 구현하는데 필요한 일부 메소드-Thread() & Thread(Runnable), Start(), Wait()-만을 구현해 보도록 하겠습니다.

우선 가장 간단한 Runnable 인터페이스부터 정의해 보도록 하죠.

#ifndef _RUNNABLE_H              //고절가주팁 #1
#define _RUNNABLE_H

struct Runnable {                // struct idiom
  virtual void Run() = 0;        // 순수가상함수
};

#endif

Java 의 인터페이스를 C++ 로 구현한다면 순수가상함수만을 갖는 추상클래스를 정의하면 된다는 걸 대부분 알고 계실 거라 생각합니다. 위와 같이 Runnable 이라는 클래스에 순수가상함수 Run()을 정의하면 Java Runnable 인터페이스와 같은 역할을 할 수 있게 됩니다. 그리고, Runnable 클래스를 정의할 때, struct 로 정의해서 public 을 쓰지 않아도 되도록 했습니다. public 멤버만 있는 클래스에 자주 쓰는 idiom 이죠.

우선 Thread 클래스의 기본 인터페이스만 먼저 정의해 보도록 하겠습니다.

01: #ifndef _THREAD_H              // 고절가주팁 #1
02: #define _THREAD_H
03:
04:
class Thread: public Runnable { // Runnable 구현
05: public:
06:   Thread();
07:   Thread(Runnable* pRunnable);
08:   void Start();
09:   void Wait();
10:   virtual void Run();          // 가상함수구현. thread 가 실행해야할
11:                                // main 로직을 정의
12: };
13:
14: #endif

04&10라인: Runnable 인터페이스를 구현해야 하니 C++ 적으로는 Runnable 클래스로부터 상속을 받아 나름대로 Run() 메소드를 구현해야 할 것입니다. 아주 기본적인 인터페이스는 일단 잡았는데요... thread 가 실행해야할 main 로직은 Run() 에 정의되는데 인스턴스 메소드이기 땜에 pthread 라이브러리에서 호출할 수가 없습니다. 이 문제는 고절가주팁 #3에서 제시된 방법을 쓰면 해결할 수 있을 겁니다. 독립된 Wrapping 방법을 쓰기 보다는 Thread 클래스에 pthread 라이브러리와 Run() 메소드를 연결해 줄 수 있는 클래스 메소드를 정의해서 이 문제를 해결해 보겠습니다.

// Thread.h
01: class Thread: public Runnable {
02: public:
03:   Thread();
04:   Thread(Runnable* pRunnable);
05:   void Start();
06:   void Wait();
07:   virtual void Run();
08:
09:   static void* Main(void* pInst);
10: };


Main() 메소드의 인터페이스는 위와 같이 정해 봤는데...어떻게 구현하면 될까요 ? 지난 글에서 제시됐던 것처럼 pInst 를 강제로 Thread 인스턴스로 형변환한 후에 Run() 메소드를 호출하면 될 것입니다. 이 아이디어를 다음과 같이 코드로 옮길 수 있습니다.

// Thread.cpp
#include "Thread.h"

void* Thread::Main(void* pInst) {
  Thread* pt = static_cast<Thread*>(pInst);
  pt->Run();
}

그럼 이제 다음 문제로 넘어가 보도록 하겠습니다. thread 를 언제 생성해야 할까요 ? Thread() 생성자에서 생성해야 할까요 ? 다음과 같이 말입니다.

// Thread.cpp
#include <pthread.h>
#include "Thread.h"

Thread::Thread() {
  pthread_t thrd;
  // Main() 클래스 메소드를 pthread의 callback 으로 등록함
  int nr = pthread_create(&thrd, NULL, &Thread::Main, this);
}

위와 같이 생성자를 작성한다면 thread 를 생성하는 순간 곧바로 thread 가 작동을 시작하게 되므로 프로그래머가 thread 를 제어하기가 힘들어 지게 될 것입니다. 게다가 우리가 처음에 설계한 인터페이스에 Start() 메소드가 있으므로 thread 가 작동을 시작하는 건 의미상 Start() 가 더 나을 것 같습니다. 그렇다면 Thread() 와 Start() 를 다음과 같이 작성하면 될 것 같습니다.

Thread::Thread() {
}

void Thread::Start() {
  pthread_t _thread;
  // 편의상 에러 체크 생략
  int nr = pthread_create(&_thread, NULL, &Thread::Main, this);
}

근데 Start() 구현을 잘 봤더니 Thread::Main() 이 굳이 public 인터페이스가 될 필요가 없을 것 같다는 생각이 듭니다. 차라리 public 인터페이스로 공개되면 Main()이 어떻게 작동하는지 모르는 프로그래머가 입력 인자인 void* 에 이상한 데이터를 넣어서 호출할 위험이 있으므로 공개하지 않는 게 훨씬 바람직할 것 같다는 생각이 드네요. 그렇다면 Main() 메소드는 private 으로 바꾸어야 할 것입니다.

class Thread: public Runnable {
public:
  Thread();
  Thread(Runnable* pRunnable);

  void Start();
  void Wait();
  virtual void Run();

private:
  static void* Main(void* pInst);

};


그럼 이제 Thread(Runnable* pRunnable) 은 어떻게 작성해야 할까요 ?

Thread() 생성자와 달리 Runnable 인스턴스를 기억하고 있어야 나중에 그 인스턴스에 대해 Run() 메소드를 호출할 수 있을 것이므로 Thread 클래스에 Runnable 인스턴스를 기억하는 멤버 변수가 하나 있어야 할 것입니다. 그렇다면 Thread 클래스 선언과 Thread(Runnable*) 생성자를 다음과 같이 수정해야겠네요.

class Thread: public Runnable {
public:
  Thread();
  Thread(Runnable* pRunnable);

  void Start();
  void Wait();
  virtual void Run();

private:
  Runnable* _runnable;

  static void* Main(void* pInst);
};


그럼 Thread(Runnable*) 생성자는 다음과 같이 작성할 수 있을 것입니다.

Thread::Thread(Runnable* pRunnable): _runnable(pRunnable) {
}


이렇게 Runnable 인스턴스를 기억하고 있으면, 나중에 Run() 에서는 다음과 같이 Runnable 인스턴스가 있는지 체크한 후에 Runnable 의 Run() 메소드를 호출할 수 있을 것입니다.

void Thread::Run() {
  if (_runnable != 0) {
    _runnable->Run();
  }
}

마지막으로 Wait() 메소드를 구현해보겠습니다. Wait() 메소드는 thread 가 종료될 때까지 호출자가 기다리도록 해주는 API 입니다. 이런 일을 해주는 pthread 라이브러리 API 로는 pthread_join() 이라는 것이 있습니다.

int pthread_join(pthread_t thread, void **value_ptr);

이 pthread_join() 을 이용해서 Wait() 메소드를 구현해 보겠습니다. 음... 그런데 구현하려고 했더니, pthread_join 의 첫번째 인자가 걸리네요. pthread_create()가 호출되고 나면 생성된 thread 에 대한 식별자 역할을 하는 pthread_t 타입의 변수를 얻게 되는데 현재로서는 Wait() 안에서 이걸 알아낼 방법은 없습니다. 그렇담 당연히 Thread 클래스 정의에 pthread_t 타입의 멤버 변수를 선언해서 Start() 안에서 pthread_create() 한 결과를 저장했다가 그걸 Wait() 에서 써야할 것 같습니다. 그럼 일단 다음과 같이 Thread 클래스 정의를 수정하고,

class Thread: public Runnable {
public:
  Thread();
  Thread(Runnable* pRunnable);

  void Start();
  void Wait();
  virtual void Run();

private:
  pthread_t _thread;
  Runnable* _runnable;

  static void* Main(void* pInst);
};


이에 따라서 생성자랑 Start() 메소드도 수정해야 합니다.

Thread::Thread(): _thread(0), _runnable(0) {
}

Thread::Thread(Runnable* pRunnable): _thread(0), _runnable(pRunnable) {
}

void Thread::Start() {
  int nr = pthread_create(&_thread, NULL, &Thread::Main, this);
}

마지막으로 Wait() 메소드는 다음과 같이 작성할 수 있습니다.

// 핵심 아이디어만 전달하기 위해 에러 체크와 메모리 관리는 생략함
void Thread::Wait() {
  void* pData;
  int nr = pthread_join(_thread, &pData);
}

지금까지 작성한 걸 합쳐보도록 하겠습니다.

// Runnable.h
#ifndef _RUNNABLE_H
#define _RUNNABLE_H

class Runnable {
public:
  virtual void Run() = 0;
};

#endif

// Thread.h
#ifndef _THREAD_H
#define _THREAD_H

#include "Runnable.h"

using namespace std;

class Thread: public Runnable {
public:
  Thread();
  Thread(Runnable* pRunnable);

  void Start();
  void Wait();
  virtual void Run();

private:
  pthread_t _thread;
  Runnable* _runnable;

  static void* Main(void* pInst);
};

#endif

// Thread.cpp
// 디버깅 코드도 조금 넣어 보겠습니다.
#include <iostream>
#include <pthread.h>
#include "Runnable.h"
#include "Thread.h"

using namespace std;

Thread::Thread(): _thread(0), _runnable(0) {
}

Thread::Thread(Runnable* pRunnable): _thread(0), _runnable(pRunnable) {
}

void Thread::Start() {
  cout << "Thread::Start() called" << endl;
  int nr = pthread_create(&_thread, NULL, &Thread::Main, this);
}

void Thread::Wait() {
  cout << "Thread::Wait() called" << endl;
  void* pData;
  int nr = pthread_join(_thread, &pData);
}

void Thread::Run() {
  if (_runnable != 0) {
    cout << "Thread::Run(): calling runnable" << endl;
    _runnable->Run();
  }
}

void* Thread::Main(void* pInst) {
  cout << "Thread::Main() called" << endl;
  Thread* pt = static_cast<Thread*>(pInst);
  pt->Run();
}


자~ 그럼 이 Thread 라이브러리를 사용하는 간단한 Application 을 짜볼까요 ?

#include <iostream>
#include <unistd.h>
#include "Runnable.h"
#include "Thread.h"

using namespace std;

class CountUpto: public Runnable {
public:
  CountUpto(int n = 10): _count(n) {
  }

  virtual void Run() {
    cout << "CountUpto: Starts to run" << endl;
    int i = 1;
    do {
      cout << "CountUpto::Run(): " << i++ << endl;
      sleep(1);
    } while (--_count);
  }

private:
  int _count;
};

class TestThread: public Thread {
public:
  virtual void Run() {
    cout << "TestThread::Run() called" << endl;

    int i = 5;
    do {
      sleep(1);
      cout << "TestThread::Run(): " << i << endl;
    } while (--i);
  }
};

int
main(void)
{
  Thread* pt1 = new TestThread();
  Thread* pt2 = new Thread(new CountUpto(20));

  pt1->Start(), pt2->Start();
  pt1->Wait(), pt2->Wait();
}


그럼 한 번 컴파일하고 실행해 볼까요 ?

$ g++ -c Thread.cpp
$ g++ -c cppthread.cpp
$ g++ -o cppthread cppthread.o Thread.o -lpthread
$ ./cppthread
Thread::Start() called
Thread::Main() called
TestThread::Run() called
Thread::Start() called
Thread::Main() called
Thread::Run(): calling runnable
CountUpto: Starts to run
Thread::Wait() called
CountUpto::Run(): 1
TestThread::Run(): 5
CountUpto::Run(): 2
TestThread::Run(): 4
CountUpto::Run(): 3
TestThread::Run(): 3
CountUpto::Run(): 4
TestThread::Run(): 2
CountUpto::Run(): 5
TestThread::Run(): 1
Thread::Wait() called
CountUpto::Run(): 6
CountUpto::Run(): 7
......
CountUpto::Run(): 18
CountUpto::Run(): 19
CountUpto::Run(): 20

이번 글을 마지막으로 C/C++ mixed programming 에 대한 이야기는 마무리 짓도록 하겠습니다. 다음 글에서는 거의 모든 개발 프로그램에서 공통적으로 필요로 하는 logging 라이브러리를 구현해 보도록 하겠습니다. 고절가주팁이 쭉~ 갈 수 있도록 많은 관심 부탁드리고, 혹시 잘못된 부분이나 보완할 부분이 있으면 언제라도 알려주세요. 여러분이 알고 있는 팁을 트랙백이나 댓글로 좀 알려 주시면 더할 나위 없이 좋겠네요.

제 글이 유익하셨다면 오른쪽 버튼을 눌러 제 블로그를 구독하세요. ->
블로그를 구독하는 방법을 잘 모르시는 분은 2. RSS 활용을 클릭하세요.
RSS에 대해 잘 모르시는 분은 1. RSS란 무엇인가를 클릭하세요.