간단히 naver.com 홈페이지를 fetch해오는 로직이 있다고 하자.

그리고 post request를 날려 이로직이 끝난뒤에 response를 받고 싶다면?


잘못된 사용 예)

    def post(self):

        self.test("test")

        print "done"


    @gen.coroutine

    def test(self, msg):

        print msg

        httpClient = AsyncHTTPClient()

        res = yield httpClient.fetch("http://www.naver.com")

        print res

        print "done"

        raise gen.Return(res) 

이렇게 하면 result는 이렇다

test

done

[web:1728] 200 POST /test (::1) 2.23ms (connection 종료, response 보냄)

HTTPResponse(...)

self.test()함수를 기다리지 않고 바로 done하고 response를 날리고 나중에 res가 도착하자 print가 되었다.

여기서 문제는 post() 함수는 기다리는 로직이 없고, 또한 asynchronous 데코도 없기때문에 로직이 끝나자 auto finish를 한것이다.


그럼 제대로 기다리려면 어떻게 해야할까? 방법은 2가지다.

1. @gen.coroutine 이용

    @gen.coroutine

    def post(self):

        res = yield self.test("test")

        print "done"


    @gen.coroutine

    def test(self, msg):

        print msg

        httpClient = AsyncHTTPClient()

        res = yield httpClient.fetch("http://www.naver.com")

        print res

        raise gen.Return(res)

yield를 써서 post함수 역시 test함수를 기다린다. 결과적으로 fetch를 기다리는것과 같다.

결과는

test

HTTPResponse

done

[web:1728] 200 POST /test (::1) 2.23ms (connection 종료, response 보냄)

이 된다.


2. @web.asynchronous 이용

이 경우에는 로직이 약간 변경된다. 기본적으로 post에서는 커넥션을 끊지 않을 뿐 post함수는 끝나고 단지 커넥션은 self.finish()를 요청하는곳에서 종료된다는것을 명시하는 것이다.

    @web.asynchronous    

    def post(self):

        self.test("test")


    @gen.coroutine

    def test(self, msg):

        print msg

        httpClient = AsyncHTTPClient()

        res = yield httpClient.fetch("http://www.naver.com")

        print res

        print "done"

        self.finish()

결과는 1번과 같다. 

'python > tornado' 카테고리의 다른 글

async request 처리  (0) 2014.04.10
@web.asynchronous @gen.coroutine VS @gen.corutine  (0) 2014.04.10

이번에 서버 개발을 하면서 파이썬 토네이도라는 프레임워크를 처음 사용해 보았다.

그러던중 async request처리시 사용하는 데코들을 보고 이에대한 궁금증이 생겼다.

@web.asynchronous
@gen.coroutine
def get(self):
    ...

이런 코드를 보고 document에서 @web.asynchronous를 찾아봤다.

If this decorator is given, the response is not finished when the method returns. It is up to the request handler to call self.finish() to finish the HTTP request. Without this decorator, the request is automatically finished when the get() or post() method returns.

이것이 기본적인 설명이다. 즉 self.finish()를 하기전까진 커넥션을 유지한다. 

asynchronous 코드는 self._auto_finish=False 이것이 핵심이다. 토네이도는 기본적으로 get, post 함수가 return될때 auto finish를 해주는데 이것을 disable시키는 것.

그런데 아래와 같은 설명이 있다.

This decorator is unnecessary if the method is also decorated with @gen.coroutine (it is legal but unnecessary to use the two decorators together, in which case @asynchronous must be first).

이런말이 쓰여있다. @gen.coroutine을 쓰면 async 데코를 쓸필요가 없다고? 뭐지? coroutine에서 항상 self.finish()를 해준다는 말인가? 그럼 이렇게 해도 self.finish()를 요청할 필요가 없나?

@web.asynchronous
def get(self):
    self.test()
    #self.finish()
@gen.coroutine def test(self): httpClient = AsyncHttpClient() val = yield httpClient.fetch("http://www.google.com") print(val)

아니다. 위의경우 는 self.finish()를 해줘야 커넥션이 종료되었다.


그럼 뭐지? stackoverflow에 질문을 올려보았다.

답은 이렇다. coroutine을 보면 이렇게 구현되어있다.

TracebackFuture instance를 생성하고 이것을 return 해준다.

def coroutine(func):
     """Decorator for asynchronous generators.
        ...
     """
     @functools.wraps(func)
     def wrapper(*args, **kwargs):
         runner = None
         future = TracebackFuture()

그리고 asynchronous를 보면 이렇게 구현되어있다.

if isinstance(result, Future):
    # If @asynchronous is used with @gen.coroutine, (but
    # not @gen.engine), we can automatically finish the
    # request when the future resolves.  Additionally,
    # the Future will swallow any exceptions so we need
    # to throw them back out to the stack context to finish
    # the request.
    def future_complete(f):
        f.result()
        if not self._finished:
            self.finish()
    IOLoop.current().add_future(result, future_complete)

즉, Future 인스턴스이고 finish가 안되었다면 finish를 해준다는 얘기다.

따라서, 연속으로 데코를 같이 쓴경우에 coroutine데코를 쓴 함수가 Future instance가 되어 auto finish가 되게 구현되있기 때문에 함께 데코를 다는것은 무의미 하다는 것!


*번외로 coroutine과 yield의 관계를 보면,

python에서 yield를 만나면 그 함수 또는 객체의 context를 유지한체 generator로 만들어 return시킨다. (generator란 next()함수를 통해서 다음 녀석이 결과물로 나오는 놈이라고 일단 알아두자.) 이 return된 객체는 추후 next()를 통해서 yield가 쓰인 부분을 실행해서 결과값으로 내놓는다. yield가 두번쓰였다면 next()를 두번 호출해야 두 yield에 걸린 놈들이 모두 실행된다.

예) http://stackoverflow.com/questions/1756096/understanding-generators-in-python

보통은 yield만 쓰면 generator가 되지만 tornado에서는 coroutine이나 engine decorator를 써야지만 yield를 쓸수 있게 해놨다. 어차피 yield를 쓰면 누군가가 next를 해줘야 할텐데, 여기선 async로 일을 처리하기 위한거니 자신이 next를 호출하는건 무의미할 것이다. 따라서 next를 실행시켜주는 녀석이 coroutine이다(정확히는 coroutine에서 실행하는 Runner의 run). 근데 여기서 주의할 것은! 이 Runner가 쓰레드를 만드는 것이 아니라 그냥 직접 next를 호출해서 결과를 실행한다는 것! 따라서 오래걸리는 작업이라고 해서 무조건 coroutine을 쓰면 안된다.

따라서 yield에 붙는 함수자체가 async로 만들어진 경우에만 coroutine과 yield를 써서 async하게 쓸수 있는 것이다. 아래 예시에서 볼수 있듯이  AsyncHttp같은녀석이 있어야 한다.

http://www.tornadoweb.org/en/stable/gen.html

* tornado에서 coroutine과 yield는 async request를 수행한다음에 response를 받아 사용해야할 일이 있을 경우 사용하는 것이다. 만약 그냥 로그를 남긴다던지 하는 굳이 결과를 받아 후처리할 것이 없는 작업들은 coroutine과 yield로 만들 필요가 없다. (물론 실패했을때 후처리를 위해서는 필요하겠지만) 

이렇게 얘기하는 이유는 coroutine은 future object의 작업이 끝날때까지 기다렸다가 작업이 끝나면 yield point로 결과물을 던져주는 역할을 하는 녀석이라는 것을 말하기 위해서다.

coroutine을 보면 Runner에서 run()을 실행하는 구간이 있는데 이때 future object를 generator의 결과물로 받아 YieldFuture object를 만들어주고 start를 실행한다. 이때 future object가 끝날때까지 ioloop에 스케쥴을 걸고 future가 끝날때 까지 기다리게 된다.

종합해서 말하자면, async job은 보통 AsyncHttpRequest와 같은 로직이 없이 결과물 도착 event를 기다리는 작업에 해당하며, 만약 async job이 오래걸리는 loop를 포함한다면 싱글 쓰레드기 때문에 그 loop에 있는동안은 다른 reqeust들이 block 당한다. 그리고 async job을 걸어놓고 다른 request를 처리하가 async job이 끝난뒤에 처리할 일이 있으면 coroutine과 yield를 쓰고, 만약 후처리 할것이 없다면 그냥 async job만 실행시킨다. 

*yield에 대해서 좀더 알고싶다면 -> http://stackoverflow.com/questions/231767/the-python-yield-keyword-explained

'python > tornado' 카테고리의 다른 글

async request 처리  (0) 2014.04.10
@web.asynchronous @gen.coroutine VS @gen.corutine  (0) 2014.04.10

+ Recent posts