Lacti's Archive

비동기 프로그래밍 패턴 2

November 19, 2013

위 이야기에 이어, async 작업과 then 작업 간의 상태 공유에 대해서 알아보자. async에서 then으로 상태를 전달하는 가장 기본적인 방법은 반환값을 사용하는 방법이다. 다른 방법으로는 lambda function에 의한 variable capture가 있겠다.

// method 1: using return value
std::async([] () { return 1; })
    .then([] (std::future<int> f) { return f.get() + 1; })
    .then([] (std::future<int> f) { return f.get() + 1; });

// method 2: using variable capture by lambda function
int result = 0;
std::async([&result] () { result = 1; })
    .then([] () { ++result; })
    .then([] () { ++result; });

반환 값으로 모든 context를 전달하는 것에는 한계가 있기 때문에 capture를 통한 방법이 더 편할 수 있다.

하지만 capture되는 변수라는 것도 결국 접근 가능한 scope 내에 있을 때 컴파일러가 해줄 수 있는 것이기 때문에 요청해야 할 비동기 작업들이 여러 함수에 걸쳐서 분포할 경우에는 위 방법으로 구현하기도 간단하지는 않다. 따라서 직접 context와 수행할 작업을 묶어 async_worker class를 구현하는 경우도 있다.

asyncworker는 대충 context + asyncwork라고 생각하면 된다.

class my_async_worker {
public:
    // async works
    void work1();
    void work2();
    void work3();
private:
    // contexts
    int _context1;
    std::string _context2;
};

즉 수행할 작업을 모두 멤버 함수로 넣고, 필요한 정보를 멤버 변수로 넣는다. 그리고 객체를 생성하고, 각 작업들을 비동기로 수행하고, 모두 완료되면 객체를 메모리에서 해제한다.

asyncworker 개념은 단순하고 직관적이다. 마치 functor를 만들기 위해 struct를 하나 만들고 operator ()를 구현하는 것과 비슷하다. 그리고 struct functor를 lambda function으로 바꾸듯, 대부분의 경우 asyncworker는 async/then 패턴으로 코드를 정리할 수 있을 것이다.

약간의? 차이가 있다면

  • async는 그 자체가 오래 걸리는 작업인 경우가 많은데,
  • async_worker의 work() 함수들은 다른 비동기 작업의 완료(completion)에 의해 callback으로 불리는 경우도 있고,
  • 각기 다른 객체의 수행 흐름에서 불리는 경우도 있다.

async_worker 패턴은 stackless coroutine 구현으로도 사용될 수 있다. stackless coroutine은 stack을 갖지 않는 coroutine으로 c#의 async/await을 생각하면 된다. 즉 instruction pointer와 stack pointer를 치환하는 방식의 coroutine이 아닌 compiler의 code generation을 통해 multiple entry/return을 구현하는 방식이라고 생각하면 되겠다. (자세한 이야기는 coroutine 이야기에서 다루도록 하겠다)

쉬운 접근을 위해 my_async_worker 코드를 다음과 같이 고쳐보도록 하겠다.

class my_async_worker {
public:
    bool execute() {
        if (_state == 0) { work1(); _state = 1; return true; }
        if (_state == 1) { work2(); _state = 2; return true; }
        if (_state == 2) work3();
        return false;
    }
private:
    int _state;
    // functions
    // contexts

비동기 함수의 context와 function을 private으로 갖고, execute() 함수만 노출되어 있다. 그리고 execute() 함수가 불릴 때마다 state가 전이되어 차례로 work1, 2, 3이 호출될 수 있도록 해준다.

  • my_async_worker::execute()를 처음 불러서 work1() 작업을 수행한다.
  • 그리고 work1() 작업에 대한 completion이 도착하면 다시 execute()를 불러서 work2() 작업을 수행한다.
  • 그리고 work2() 작업에 대한 completion이 도착하면 다시 execute()를 불러서 work3() 작업을 수행한다.
  • 이 때 execute()는 return을 반환하므로 더 이상 수행할 작업이 없는 것이다. 따라서 my_async_worker 객체를 메모리에서 제거한다.

위와 같은 state machine은 기계적으로 생성이 가능하기 때문에 compiler에서 위와 같은 방법을 사용하여 구현한 c#의 async/await이 있는가 하면 c에서는 macro으로 구현한 것도 있다.

async를 통해 비동기 작업을 시작한 후 then으로 이어할 작업을 설정하는 async/then 패턴과, 수행할 비동기 작업과 context를 묶어서 하나의 객체로 구성하는 async_worker 패턴을 간단히 알아보았다.

각자의 기본 개념을 적절히 이해하여 각 상황에 맞게 적절히 사용하면 되겠다.

Loading script...