C++11 Future와 Async

|

Thread의 결과값 획득하는 방법

std::futurestd::promise를 이용하면 다른 Thread의 결과값을 쉽게 획득할 수 있습니다. Thread에서 연산을 완료한 후 그 결과값은 promise에 저장합니다. 이후, future를 이용해서 그 값을 획득할 수 있습니다.

Thread 결과값은 다음과 같은 코드를 이용해서 획득가능합니다. Thread의 결과값을 받을 때까지 get() 부분은 Blocking 되어 대기합니다.

  future<T> fut = ...; 
  T res = fut.get();

promise의 사용은 다음과 같이 할 수 있습니다.

  promise prom = ...;
  T val = ...;
  prom.set_value(val);


async 함수

async 함수를 사용하면 특정 함수 등을 Thread로 구동시키고 그 결과를 리턴받을 수 있습니다. 다음과 같은 예제 코드를 살펴보면,

#include <cstdio>
#include <future>

using namespace std;

int fun() {
  for (int i = 1; i <= 10; i++) {
    printf("fun[%d]\n", i);
  }

  return 200;
}

int main() {
  //auto fut = async(fun);
  auto fut = async(launch::async, fun);
  //auto fut = async(launch::deferred, fun);

  for (int i = 1; i <= 10; i++) {
    printf("main[%d]\t", i);
  }
  printf("\n");

  int result = fut.get();

  printf("result : %d\n", result);

  return 0;
}

async를 통해 실행한 결과값을 get() 함수를 이용해서 돌려받을 수 있는 것을 확인할 수 있습니다.

위 예제에서 async 호출하는 부분을 3가지 예시로 들었는데, 만약

  auto fut = async(launch::async, fun);

으로 수행하면, 함수 fun()은 즉시 실행이 되고 그 결과는 future에 저장이 됩니다. (실행해보면 메인 Thread와 별도 Thread가 동시에 돌아가는 것을 확인할 수 있습니다.)


만약, async 부분을 다음과 같이 호출한 경우는

  auto fut = async(fun);    // 또는
  auto fut = async(launch::deferred, fun);

fun() 함수는 바로 실행되는 것이 아니라 ‘int result = fut.get()’ 코드가 실행될 때 fun() 함수가 실행되는 것을 확인할 수 있습니다.

C++11 조건 변수(Condition Variable)

|

조건 변수

조건 변수는 헤더 파일을 `include` 해야 사용할 수 있습니다.

조건 변수를 사용하면 특정 조건이 만족될 때까지 현재 Thread를 Blocking 할 수 있습니다. 이 때 특정 조건으로는 notify 이벤트를 주거나 타임 아웃 등이 될 수 있습니다.

조건 변수는 각 Thread를 Blocking 함으로 호출 순서를 조절하게 해서 결과적으로는 Thread 간 통신을 가능하게 해주는 효과를 가집니다.


조건 변수의 구성

조건 변수는 크게 wait()notify_one()이나 notify_all() 함수를 세트로 구성되어집니다.

notify_one() 함수는 해당 조건 변수를 기다리고 있는 Thread들 중 한 개의 Thread를 깨웁니다. notify_all() 함수는 조건 변수를 기다리는 모든 Thread를 깨웁니다.

wait()를 호출하는 Thread는 먼저 락 객체를 점유하고 있는 상태여야 합니다. wait()를 호출하면 해당 락 객체의 unlock()이 호출되고 Thread가 Blocking 됩니다.


조건 변수의 활용

조건 변수를 가장 잘 활용할 수 있는 예제로 메세지 큐(Message Queue)를 들 수 있습니다.

다음과 같은 코드를 이용해서 Queue에 명령을 집어넣고 notify_one() 이벤트를 날립니다.

  void enqueue(T t) {
    unique_lock<std::mutex> lock(mMutex);
    mQueue.push(t);
    mCondition.notify_one();
  }


또한 해당 Thread는 메세지를 처리하고, 다시 wait()로 Blocking 상태로 들어갑니다.

  T dequeue(void) {
   unique_lock<std::mutex> lock(mMutex);
    while((mIsLoop)&&(mQueue.empty())) {
      mCondition.wait(lock);
    }
    T val = mQueue.front();
    mQueue.pop();
    return val;
  }

C++11 상호 배제(Mutex)

|

Mutex 종류

C++ 표준에서는 Mutex 종류는 크게 2가지가 있습니다. 하나는 일반 Mutex이며, 다른 하나는 타임 아웃 Mutex 입니다.


일반 Mutex

일반 Mutex에는 std::mutex, std::recursive_mutext 두 종류가 있습니다. 두 클래스는 공통으로 다음과 같은 함수를 갖고 있습니다.

  • lock() : Thread에서 락 점유를 시도하며, 락을 획득할 때까지 무한정 대기함.
  • try_lock() : 락 점유를 시도하며, 성공시 true, 실패시 false를 바로 리턴함.
  • unlock() : 락을 해제함.

std::mutex는 중복해서 lock()을 호출할 경우 ‘데드락(Deadlock)’ 상황에 빠질 수 있습니다.

std::recursive_mutexlock()을 여러 번 호출해도 상관없지만, 대신 락을 해제할 때는 동일한 횟수만큼 unlock()을 호출해주어야 락이 해제됩니다.


타임 아웃 Mutex

타임 아웃 Mutex에는 std::timed_mutex, std::recursive_timed_mutex가 있습니다. 그리고 다음과 같은 함수들을 갖고 있습니다.

  • lock() : Thread에서 락 점유를 시도하며, 락을 획득할 때까지 무한정 대기함.
  • try_lock() : 락 점유를 시도하며, 성공시 true, 실패시 false를 바로 리턴함.
  • unlock() : 락을 해제함.
  • try_lock_for(rel_time) : 주어진 시간까지 락 점유를 시도. 시간은 상대 시간임. 성공시 true, 실패시 false를 바로 리턴.
  • try_lock_until(abs_time) : 주어진 시간까지 락 점유를 시도. 시간을 절대 시간임. 성공시 true, 실패시 false를 바로 리턴.


lock 클래스

Mutex를 좀 더 사용하기 쉽게 Wrapping한 클래스입니다. lock 클래스의 소멸자는 자동으로 내부의 Mutex를 해제해주기 때문에, 각 함수의 로컬 변수로 사용할 경우 함수를 빠져나오면서 자연스럽게 락이 해제됩니다. 즉, 락 해제에 신경을 덜 쓸 수 있습니다.

lock_guard는 생성자에서 락 점유를 시도하며, 점유를 성공할 때까지 Blocking 됩니다.

unique_lock은 객체를 미리 선언해두고, 나중에 필요한 시점에 락 점유를 시도할 수 있습니다. lock(), try_lock(), try_lock_for(), try_lock_until() 등의 함수도 제공합니다.


lock 예제

#include <mutex>

using namespace std;

class LockExample {
 public:
  void sendMessage(int what, int arg1, int arg2);

 private:
  mutable mutex mMutex;
};

void LockExample::sendMessage(int what, int arg1, int arg2) {
  unique_lock<mutex> lock<mMutex>

  mMessageSender->sendMessage(what, arg1, arg2);
}


이중 검사 락 알고리즘

멀티 쓰레드 환경에서 어떤 부분이 단 한 번만 호출이 되어야 한다면 이중 검사 락 알고리즘을 사용할 수 있습니다. 물론, call_once()를 사용한다면 굳이 이 알고리즘을 사용할 필요는 없을 것 같습니다.

아래 예제는 isInitialized 변수를 확인한 다음 초기화를 수행하고 isInitialized 변수의 값을 true로 세팅하는 예제입니다. isInitialized 변수가 true로 세팅되기 직전에 다른 쓰레드가 끼어드는 것을 방지하기 위해서 이중 검사 락 알고리즘을 사용했습니다.

#include <cstdio>
#include <thread>
#include <mutex>
#include <vector>

using namespace std;

class LockExample {
 public:
  void init() {
    printf("Init()\n");
  }
};

bool isInitialized = false;
mutex mtx;

LockExample exam;

void func() {
  if (isInitialized == false) {
    unique_lock<mutex> lock(mtx);

    if (isInitialized == false) {
      exam.init();
      isInitialized = true;
    }
  }

  printf("func()\n");
}

int main() {
  vector<thread> threads;

  for (int i = 0; i < 5; i++) {
    threads.push_back(thread{func});
  }

  for (auto &t : threads) {
    t.join();
  }

  return 0;
}

call_once를 이용한 Singleton 패턴

|

Thread-Safe Singleton

기본적으로 Singleton 패턴은 Thread-Safe 하지 않습니다. 그래서 생성자에 synchronized 키워드나(Java의 경우) ‘Mutex’ 등을 이용해서 Thread-Safe 하도록 만들어주는 경우가 많습니다. 그게 아니면, 프로그램 실행 초반에 인스턴스를 생성하도록 하는 방법을 많이 씁니다.

C++11 부터 사용가능한 call_once()를 이용하면 좀 더 간편하게 Singleton 패턴을 사용할 수 있습니다.

std::call_once()std::once_flag()와 함께 사용하여 복수의 Thread 환경에서 특정 함수를 단 한 번만 구동되도록 할 수 있습니다. 이러한 호출을 ‘Effective Call Once Invocation’라고 합니다.


Singleton.h

#ifndef SNOWTHREAD_SINGLETON_H
#define SNOWTHREAD_SINGLETON_H

#include <cstdio>
#include <mutex>
#include <memory>

using namespace std;

class Singleton {
 public:
  static Singleton &getInstance() {
    call_once(Singleton::mOnceFlag, []() {
      printf("Singleton Instance is created...\n");
      mInstance.reset(new Singleton);
    });

    return *(mInstance.get());
  }

  void log() {
    printf("hello\n");
  }


 private:
  static unique_ptr<Singleton> mInstance;
  static once_flag mOnceFlag;

  Singleton() = default;
  Singleton(const Singleton &) = delete;
  Singleton &operator=(const Singleton &) = delete;
};

unique_ptr<Singleton> Singleton::mInstance;
once_flag Singleton::mOnceFlag;

#endif //SNOWTHREAD_SINGLETON_H


main.cpp

#include "Singleton.h"

int main() {
  Singleton::getInstance().log();
  Singleton::getInstance().log();

  return 0;
}


실행 결과

Singleton Instance is created...
hello
hello

C++11 Thread 생성 방법들

|

함수 포인터를 이용하는 방법

함수 포인터를 이용하는 예제 코드는 다음과 같습니다.

#include <cstdio>
#include <thread>

using namespace std;

void counter(int id, int length) {
  for(int i=1; i<=length; i++) {
    printf("counter[%d] : %d\n", id, i);
  }
}

int main() {
  thread t1(counter, 1, 5);
  thread t2(counter, 2, 7);
  t1.join();
  t2.join();

  return 0;
}

위에서 join()은 각 Thread가 작업 완료될 때까지 Blocking되어 있도록 하는 명령어입니다. Blocking은 일반적으로 자원의 낭비를 가져오기 때문에 실제 프로그램에서는 join()의 사용을 최대한 피하는 것이 좋습니다. 대신 Thread에 메세지(Message)를 처리하는 루틴을 만들고, Thread에 메세지를 보내어서 작업을 수행하는 방식이 좀 더 바람직합니다.


함수 객체를 이용하는 방법

#include <cstdio>
#include <thread>

using namespace std;

class Counter {
 public:
  Counter(int id, int length) {
    mId = id;
    mLength = length;
  }

  void operator()() const {
    for (int i = 1; i <= mLength; i++) {
      printf("counter[%d] : %d\n", mId, i);
    }
  }

 private:
  int mId;
  int mLength;
};

int main() {
  // #1
  thread t1{Counter(1, 5)};

  // #2
  Counter c2(2, 7);
  thread t2(c2);

  // #3
  thread t3(Counter(3, 8));

  t1.join();
  t2.join();
  t3.join();

  return 0;
}

위의 예제에서 3가지 방식이 있었는데 3번째 방식은 특수한 경우(예를 들어 인자로 들어가는 클래스의 생성자에 파라메터가 없는 경우)에 컴파일 에러가 뜰 수 있기 때문에 가급적 첫 번째 방식을 사용하는 편이 더 낫습니다.


람다 표현식을 이용하는 방법

#include <cstdio>
#include <thread>

using namespace std;

int main() {
  thread t1([](int id, int length) {
    for (int i = 1; i <= length; i++) {
      printf("counter[%d] : %d\n", id, i);
    }
  }, 1, 7);

  t1.join();

  return 0;
}


클래스 메소드를 이용하는 방법

#include <cstdio>
#include <thread>

using namespace std;

class Counter {
 public:
  Counter(int id, int length) {
    mId = id;
    mLength = length;
  }

  void loop() const {
    for (int i = 1; i <= mLength; i++) {
      printf("counter[%d] : %d\n", mId, i);
    }
  }

 private:
  int mId;
  int mLength;
};

int main() {
  Counter c1(1, 7);

  thread t1{&Counter::loop, &c1};

  t1.join();

  return 0;
}

이 방법은 특정 인스턴스의 메소드를 별도 Thread로 실행시킬 수 있는 장점이 있습니다.