2016년 10월 24일 월요일

boost asio에서 iocp 버전 코드 분석

boost asio에서 iocp를 이용한 비동기 작업처리를 어떻게 구현하고 있는지를 직접 구현하면서 되짚어 보려한다.

1. start_post() - 비동기 호출 인터페이스

 먼저 비동기 작업이 완료되면 호출되는 함수 콜백과 콜백함수 호출에서 참조되는 정보를 Context라는 클래스에 정의할 때 비동기 작업의 시작은 start_post() 함수 호출로부터 시작된다.
class Context { ... };
void callback(std::shared_ptr<Context>);

void start_post() {
  std::shared_ptr<Context> c(new Context);
  // c에 콜백에 필요한 정보 초기화
  ...
  io_service.post(std::bind(callback, c));
}
 여기서 Context는 단순히 POD가 아니라 포인터 객체, stl container등 다양한 정보를 담고 있을 수 있다. 물론 이 Context객체는 콜백이 호출이 완료되는 시점까지 lifetime이 지속되어야 한다.
 참고로 boost asio는 위와 같은 폼으로 시작한 비동기 작업이 완료되면 io_service.run() 함수가 호출되었던 쓰레드에서 해당 콜백이 호출된다.
 io_service.post()는 bind를 사용한 함수객체를 인자로 받고 있는데 이는 즉, 고정된 인터페이스가 아닌 임의의 타입의 콜백을 등록할 수 있다는 의미이며 이는 템플릿을 통해 구현한다. 또한 함수객체는 임시객체로 만들고 있으므로 이를 콜백이 호출될 때까지 lifetime을 연장시키는 부분도 구현해야 한다.

2. io_service::post()

template <typename Handler>
void io_service::post(Handler && handler) {
  Handler h(std::move(handler));
  impl.post(h);
  // 위 코드 대신 impl.post(handler);를 써도 된다
}
 먼저 bind로 만들어진 임시 함수 객체를 인자로 받기에 io_service::post()는
 1. 함수 템플릿
 2. 템플릿 파라미터는 함수객체 타입인 Handler
 3. 함수 인자로는 Handler 타입의 rvalue 참조 타입
으로 정의한다.
 boost asio는 각 플랫폼 별로 별도의 io_service구현체(멤버 impl, win_iocp_io_service타입)를 두고 있기에 handler객체를 구현체에게 넘겨주어야 한다. (로컬 변수 h는 실제 boost asio구현에서는 async_result_init라는 클래스 타입의 변수이다. 여기서는 별 의미없는 코드로 아래 주석의 코드로 대체 가능하다.)
 여기서 포커스는 handler객체의 리소스 소유권을 넘겨받고 impl.post() 내에서도 인자로 넘겨진 handler 객체의 리소스 소유권을 가져가는데 있다.

3. win_iocp_io_service::post()

template <typename Handler>
void win_iocp_io_service::post(Handler & handler) {
  typedef completion_handler<Handler> op;
  op::ptr p = { malloc(sizeof(op)), 0 };
  p.p = new (p.v) op(handler);

  enqueue_worker(p.p);
  p.v = p.p = 0; //ptr소멸자에서 reset()때 메모리 free되는것 방지
}
 이 함수는 인자로 받는 handler를 rvalue 참조가 아닌 lvalue 참조로 받았다. 무엇이 되었든지 우리는 이 객체의 소유권을 가져올 것이다. 그런데 iocp queue에 데이터를 넘겨주고 받을 길은 OVERLAPPED 구조체를 통한 길 뿐이다. 그래서 Handler타입의 객체를 scope를 벗어나도 소멸자의 영향을 받지 않으면서 객체의 모든 정보를 담을 수 있는 방법을 쓴다.

 1. Handler 타입 크기만큼의 메모리를 할당하고
 2. 할당된 메모리를 Handler타입의 객체인 것처럼 사용하여 handler의 모든 리소스를 새로 할당한 메모리 객체로 옮겨온다. (in-place new operator, rvalue ref. constructor)
 3. enqueue_worker()함수는 새로 할당된 메모리의 데이터를 가지고 실제 비동기 작업을 수행하기 위해 작업큐에 등록

 이제 인자의 handler객체의 리소스 소유권은 op::ptr 타입 객체의 멤버 p가 가리키는 새로운 Handler 객체로 넘어갔으며 p는 실제로는 포인터이므로 함수를 벗어나도 Handler 소멸자에 의해 리소스가 영향을 받지 않게 된다. (물론 이 방식으로는 콜백이 호출이 끝나면 명시적 소멸자를 호출해주어야 한다. 이후 자세히 설명)

4. OVERLAPPED, win_iocp_operation, completion_handler<Handler>

LPOVERLAPPED overlapped;
GetQueuedCompletionStatus(..., &overlapped, ...);

// overlapped를 어떤 형태로 변형하고 이 변형된 타입이 handler 콜백 호출
 앞에서 언급한대로 iocp에서 비동기작업에 필요한 정보를 전달할 방법은 OVERLAPPED 구조체가 뿐이다. 또한 작업이 완료되면 위 코드에서처럼 OVERLAPPED 구조체 포인터로 post()시점에 넘겨준 정보를 받고 이를 가지고 어딘가 있을 handler 콜백을 호출해야 한다.
 결론만 얘기하면 overlapped 포인터를 비템플릿 타입인 win_iocp_operation 타입으로 캐스팅하고 win_iocp_operation의 멤버함수를 호출함으로써 적절한 handler를 호출되도록 OVERLAPPED구조체로부터 상속한 클래스를 만든다.


class win_iocp_operation : public OVERLAPPED {
public:
  void complete() { func(this); }
protected:
  typedef void(*func_type)(win_iocp_operation*);
  win_iocp_operation(func_type f) : func(f) { }
private:
  func_type func;
};
func_type타입의 함수 포인터를 인자로 받는 클래스를 OVERLAPPED 구조체로부터 상속한다. 그러면 앞에서 overlapped포인트는 다음과 같이 쓸 수 있다.

win_iocp_operation * op = static_cast<win_iocp_operation*>(overlapped);
op->complete();
그리고
template <typename Handler>
class completion_handler : public win_iocp_operation {
public:
  completion_handler(Handler & h)
    : win_iocp_operation(&completion_handler::do_complete)
    , handler(std::move(h))
  {}
  static void do_complete(win_iocp_operation * op) {
    completion_handler * h(static_cast<completion_handler*>(op));
    // p는 그냥 로컬변수쯤으로 여기자. 자세한 설명은 뒤에
    ptr p = { h, h };
    Handler handler(std::move(h->handler));
    // h가 가지고 있던 모든 리소스를 handler로 넘기고 p,v메모리를 해제한다.
    // 이유는 아래 콜백handler()가 예외를 던질때를 대비해서 미리 해제를 하는듯하다
    p.reset();
    handler();
  }
private:
  Handler handler;
};
 completion_handler클래스는 Handler타입의 핸들러를 인자로 받는 클래스이다. 이로 보아 win_iocp_operation::complete()가 호출될 때 completion_handler<Handler>::do_complete()가 호출됨을 알 수 있다. 이렇게 함으로써 콜백 호출시점엔 직접적으로 Handler타입을 몰라도 올바른 타입의 handler가 호출될 수 있는 구조가 만들어진다.

 win_iocp_operation과 completion_handler<Handler>의 관계는 한마디로 요약하면 completion_handler<Handler>는 OVERLAPPED+콜백정보를 만들때, win_iocp_operation은 작업이 완료되고 콜백이 호출될 때에만 사용되는 것으로 용도가 구분됨을 알 수 있다.

5. struct completion_handler<Handler>::ptr

 위에 compltion_handler<Handler>::ptr 구조체는 헬퍼 클래스로 completion_handler 내부 클래스로 다음과 같은 모양이다.
struct ptr {
  void * v;
  completion_handler * p;
  ~ptr() { reset(); }
  void reset() {
    if (p) { p->~completion_handler(); p = 0; }
    if (v) { free(v); v = 0; }
  }
};
 ptr 타입 변수 p,v에 할당된 메모리를 ptr가 소멸될 때 적절히 해제해주기 위한 도구 정도로 보면 되겠다.

2016년 10월 19일 수요일

std::forward의 정체

NOTE: http://en.cppreference.com/w/cpp/utility/forward

std::forward<T>는 C++11에 추가된 템플릿 함수인데 언뜻보면 전달한다의 느낌 정도인데 막상 언제 써야하는지는 와닿지 않는다.

template <class T>
T&& forward(typename std::remove_reference<T>::type & t);
template <class T>
T&& forward(typename std::remove_reference<T>::type && t);
std::forward 함수는 위와 같이 두가지 버전이 있다. 인자의 타입 T에 따라 선택된다. 즉, 인자가 lvalue 이면 첫번째, 그렇지 않으면 두번째 버전이 선택되게 된다.

우선 forward가 하는 일은 함수가 호출될 때 인자가 가지고 있었던 타입 정보를 그대로 다른 함수 호출 때 넘겨준다.
공식적인 정의는 아니지만 경험상 이해하기 쉬운 설명이다. 여기서 말하는 타입정보는 정확히는 value category인데 C++ 표준에서 lvalue, prvalue, xvalue 등으로 분류한다.

타입추론

 먼저 함수 템플릿 인자 추론과정에서 추론을 시작하기 전에 함수 인자 타입P와 함수 호출 인자 A에 대해 몇가지 조정을 한다.
1. P가 참조 타입이 아니면
  a) A가 배열이면 포인터 타입으로 변환
  b) A가 함수타입이면 포인터 타입으로 변환
  c) A가 cv 한정사가 붙었다면 최상위의 cv한정사는 무시
template <class T> void f(T);
int a[3];
f(a);  // 1-a) P=T, A=int[3] -> int* : T=int*
const int b = 13;
f(b);  // 1-c) P=T, A=const int -> int : T=int
void g(int);
f(g);  // 1-b) P=T, A=void(int) -> void(*)(int) : T=void(*)(int)
2. P가 cv한정사가 붙은 타입이면 최상위 cv무시
3. P가 참조이면 P에 의해 참조되고 있는 타입을 사용
4. P가 cv한정사가 없는 rvalue 참조이고 함수호출에 넘겨진 인자가 lvalue이면 A대신 A의 참조 타입을 사용 (c++ standard 14.8.2.1)
template <class T> int f(T&&);  // P=cv한정사없는 rvalue참조
template <class T> int g(const T&&);  // P=cv 있는 rvalue 참조
int i;
f(i);  // P=T&&->T,A=int->int& : int f<int&>(int&)
f(0);  // P=T&& -> T,A=int : int f<int>(int&&)
//g(i);  // P=const T&&->T&&->T,A=int : int g<int>(const int &&), i(lvalue)를 const int &&에 바인딩 불가
이 과정이 끝났을 때 함수 파라미터 타입 P=A가 되게 하는 타입들을 찾는 추론과정을 거친다.

std::forward 인스턴스

cppreference에 forward함수에 대한 예제의 일부다.

class X {
public:
  X(int&& n) { std::cout << "rvalue overload, n=" << n << "\n"; }
  X(int& n)  { std::cout << "lvalue overload, n=" << n << "\n"; }
};

template<class T, class U>
std::unique_ptr<T> make_unique1(U&& u)
{
    return std::unique_ptr<T>(new T(std::forward<U>(u)));
}
 
int main()
{   
    auto p1 = make_unique1<X>(2); // rvalue
    int i = 1;
    auto p2 = make_unique1<X>(i); // lvalue
}
make_unique1이 인스턴스화 되는 과정을 보면
make_unique1(1);  // P=U&&->U,A=int : make_unique1<X, int>(int&&)
make_unique1(i);  // P=U&&->U,A=int->int&, make_unique1<X, int&>(int&)
(위 마지막에 U=int&로 추론되어 make_unique1<X, int&>(int& &&);가 되나 reference collapsing rule(c++ standard 8.3.2)에 의해 int&가 되었다)

 위와 같이 인스턴스와 될 경우 forward함수는 각각
int&& forward(int&&);  //make_unique1<X,int>(int&&)
int& forward(int&);  //make_unique1<X,int&>(int&)
가 호출되게 된다. make_unique1함수 내에서는 파라미터 u는 호출시점의 인자의 본래 value category를 상실하게되지만 템플릿 인스턴스화 과정으로 본래 타입정보를 forward를 통해 유지하고 그 타입 정보를 통해 내부에서 호출하는 다른 함수들에 대해 적절한 overloading(여기서는 std::unique_ptr<T>)이 선택될 수 있도록 해주는 역할을 한다.

 당연하겠지만 forward함수자체는 뭔가 특별한 일을 하는게 아니라 forward의 인자타입과 리턴타입 정보를 유지해주는 부분이 핵심인 것이다. 실제로 forward함수 구현은 달랑 한줄이다.(libstdc++ 5.3.1)
template <class T>
T&& forward(typename std::remove_reference<T>::type & t)
{
  return static_cast<T&&>(t);
}
template <class T>
T&& forward(typename std::remove_reference<T>::type && t)
{
  return static_cast<T&&>(t);
}

언제 forward를 써야 하나

1. 함수 템플릿을 정의하려고 한다.
2. 이 함수 템플릿 내부에 어떤 함수 X를 호출하는데 X의 인자 타입이 rvalue참조인것과 그렇지 않은 여러버전의 오버로딩이 존재하고 우리 함수의 인자에 따라 X의 오버로딩중 적절한 것을 선택해 호출할 필요가 있다.

위 조건을 만족한다면 주저말고 forward를 써야한다.

2016년 9월 29일 목요일

acquire / release fence의 핵심

NOTE: Preshing 블로그의 DCLP관련 포스팅의 요약입니다.

1. store-release

atomic_thread_fence(release)
atomic store(relaxed)
=>
atomic store(release)

2. load-acquire

atomic load(relaxed)
atomic_thread_fence(acquire)
=>
atomic load(acquire)

3. acquire semantics, release semantics
acquire semantics는 공유 메모리에서 읽기하는 작업에만 적용될 수 있는 속성, 그 읽기 작업을 read-acquire라 한다.
release semantics는 공유 메모리에 쓰기 작업에만 적용될 수 있는 속성, 그 쓰기 작업을 write-release라 한다.

acquire semantics는 프로그램 순서상 read-acquire와 그 이후의 어떠한 메모리 읽기/쓰기와 메모리 재배치를 막는다.
비슷하게 release-semantics는 프로그램 순서상 write-release와 그 이전의 어떠한 메모리 읽기/쓰기와 메모리 재배치를 막는다.

4. 이걸로 무엇을 할 수 있나
DCLP가 가장 직접적인 예일듯. 다음은 Preshing 블로그에 나와있는 예다.

singleton패턴의 getInstance() 함수다. 인스턴스를 포인터로 만드는 케이스.
Singleton* Singleton::getInstance() {
    Lock lock;      // scope-based lock, released automatically when the function returns
    if (m_instance == NULL) {
        m_instance = new Singleton;
    }
    return m_instance;
}
우리는 보다 나은 성능을 위해 Lock(mutex 같은)을 사용하지 않고 구현하려 한다.

m_instance가 NULL인지 체크하는 부분은 getInstance()가 호출될 때마다 수행된다.
처음 m_instance를 생성할 때에는 실행 비용이 비싸도 되지만 한번 생성된 뒤에는 실행 비용이 적으면 적을수록 좋을 것. 이로부터 DCLP는 출발한다. 우선 이 기준으로 위 함수를 바꿔보면

Singleton* Singleton::getInstance() {
    Singleton* tmp = m_instance;
    ...                     // insert memory barrier
    if (tmp == NULL) {
        Lock lock;  // 아직 Lock은 있다. tmp가 NULL일때에만 Lock사용하므로 이 정도는 ok
        tmp = m_instance;
        if (tmp == NULL) {
            tmp = new Singleton;
            ...             // insert memory barrier
            m_instance = tmp;
        }
    }
    return tmp;
}
위에서 tmp 가 NULL이면 if문안에서 Lock을 건다. 그러고 나서 m_instance의 값을 다시한번 확인한다. 이는 처음 if문과 Lock 사이에 다른 쓰레드가 이미 getInstance()를 호출하고 Lock을 걸고 new를 했을 수도 있다. 다시한번 m_instance를 체크해서 여전히 NULL이라면
new를 한다.
여기서 눈여겨 볼 부분은 tmp변수와 tmp = m_instance로 실제 공유 메모리인 m_instance의 값을 tmp로 복사해 오는 부분이다. 이는 barrier(fence)를 사용하기 위한 장치다.

std::atomic Singleton::m_instance;
std::mutex Singleton::m_mutex;

Singleton* Singleton::getInstance() {
    Singleton* tmp = m_instance.load(std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_acquire);
    if (tmp == nullptr) {
        std::lock_guard lock(m_mutex);
        tmp = m_instance.load(std::memory_order_relaxed);
        if (tmp == nullptr) {
            tmp = new Singleton;
            std::atomic_thread_fence(std::memory_order_release);
            m_instance.store(tmp, std::memory_order_relaxed);
        }
    }
    return tmp;
}
위 코드가 최종 모습. 자세한 설명은 해당 블로그를 참조하고 이로써 m_instance를 통해 쓰레드간에 synchronizes-with 관계를 형성한다. 매번 getInstance()마다 Lock걸 필요가 없으면서 쓰레드간에 안전하게 초기화할 수 있다.

std::atomic Singleton::m_instance;
std::mutex Singleton::m_mutex;

Singleton* Singleton::getInstance() {
    Singleton* tmp = m_instance.load(std::memory_order_acquire);
    if (tmp == nullptr) {
        std::lock_guard lock(m_mutex);
        tmp = m_instance.load(std::memory_order_relaxed);
        if (tmp == nullptr) {
            tmp = new Singleton;
            m_instance.store(tmp, std::memory_order_release);
        }
    }
    return tmp;
}
C++11 atomic과 low level memory ordering 제약조건을 사용한 코드 형태.

2016년 7월 31일 일요일

[snippet] js heredoc

function hereDoc(f) {
  return f.toString().
      replace(/^[^\/]+\/\*!?/, '').
      replace(/\*\/[^\/]+$/, '');
}
var tennysonQuote = hereDoc(function() {/*!
  Theirs not to make reply,
  Theirs not to reason why,
  Theirs but to do and die
*/});
http://stackoverflow.com/questions/805107/creating-multiline-strings-in-javascript

2016년 7월 18일 월요일

[snippet] windows에서 svn revision값 출력하는 batch 스크립트

@set REV=unknown
@for /f "usebackq tokens=1,2 delims=: " %%A in (`svn info`) do @if "%%A" == "Revision" @set REV=%%B
@echo %REV%

2016년 7월 11일 월요일

[snippet] blob데이터를 파일로 다운로드

    var a = document.createElement("a");
    document.body.appendChild(a);
    a.style = "display: none";
    
    var data; // some data
    var json = JSON.stringify(data),
        blob = new Blob([json], {type: "octet/stream"}),
        url = window.URL.createObjectURL(blob);
    a.href = url;
    a.download = 'a.txt';
    a.click();
    window.URL.revokeObjectURL(url);

2016년 5월 31일 화요일

shared_ptr, unique_ptr의 deleter로 lambda쓰기

vs2013 Update5, g++ 5.3.1

auto p1 = std::shared_ptr<int>(new int, [](void * p) {
 delete (int*)p;
});

auto p2 = std::unique_ptr<int>(new int, [](int * p) {
 delete p;
});

shared_ptr의 경우 템플릿 파라미터에 deleter의 타입을 주지 않아도 생성자의 deleter 타입을 추론해서 포인터가 적절히 생성된다. 내부적으로 shared_ptr객체 자체가 아니라 다른 곳에 deleter를 저장하고 그에 대한 포인터를 가진다.

하지만 unique_ptr의 경우 포인터 객체 내부에 deleter를 저장하며 템플릿 인자로 deleter의 타입을 주지 않을 경우 기본적으로 std::default_delete<int>가 사용되도록 되어 있다. 그래서 lambda를 std::default_delete<int>로 변환이 안되므로 에러가 나게 된다. 이를 해결하려면 shared_ptr와 다르게 unique_ptr의 deleter 타입을 명시적으로 지정해주어야 한다.

auto p2 = std::unique_ptr<int, void(*)(int*)>(new int, [](int * p) {
 delete p;
});