[컴][파이썬] Tornado 의 coroutine

토네이도 웹서버 / web server tornado


Tornado 의 coroutine

Tornado 에서 asychronous 한 code 를 짤 때 coroutine 을 이용하라고 권한다. [ref. 1]
그래서 사용방법을 대략적으로 알아보자.


사용방법

@gen.coroutine 은 tornado 에서 callback 을 사용하는 code 를 yield 를 사용하는 모습으로 변경시켜주는 wrapper 정도로 생각하면 될 듯 하다.

@gen.coroutine 를 사용하면 아래의 code 를

class AsyncHandler(RequestHandler):
    @asynchronous
    def get(self):
        http_client = AsyncHTTPClient()
        http_client.fetch("http://example.com",
                          callback=self.on_fetch)

    def on_fetch(self, response):
        do_something_with_response(response)
        self.render("template.html")


이렇게 바꿀 수 있다.
from tornado import gen
@gen.coroutine
def fetch_coroutine(url):
    http_client = AsyncHTTPClient()
    response = yield http_client.fetch(url)
    # In Python versions prior to 3.3, returning a value from
    # a generator is not allowed and you must use
    #   raise gen.Return(response.body)
    # instead.
    return response.body



개인적으로는 yield 가 더 혼란스럽긴 한데, 여튼 대략적으로 설명하면, 처음에 get() 이 호출되고,  fetch 를 하면 fetch 는 network 작업이기 때문에 response 가 올 때까지 local 은 할일이 없다. 그러기 때문에 이 시간동안에 다른 일을 하고, 끝나면, callback 인 self.on_fetch() 를 호출하면 된다. 이 때 다른일이라 함은 get() 의 호출이 끝난 후 다음 code 가 된다.

그런데 yield 를 사용하게 되면, callback 부분이 get() 안으로 들어온다. 의미자체는 get() 이 호출될 때 할 일들을 한번에 적어놓는다는 의미에서 나름 명확한 면은 있지만, "source 에서 fetch() 가 끝나고 yield 를 해준 후의 흐름"을 파악하기가 애매한 부분이 있다.

gen.coroutine source

coroutine 을 사용할 때는 아래와 같은 흐름을 갖는다.
  1. result = func(*args, **kwargs) --> fetch_coroutine
  2. yield --> result = func(*args, **kwargs) 로 돌아온다.
  3. yielded = next(result)
  4. Runner(result, future, yielded)
  5. Runner.run() 에서 일을 마치면,  send() 를 해준다.
그래서 yield 뒤에 오는 함수는 대체로 future 를 갖는다. list, 나 dict 도 가능하지만, 이것도 실제로는 gen.coroutine 내에서 future 로 변경시켜 준다.



def coroutine(func, replace_callback=True):
    return _make_coroutine_wrapper(func, replace_callback=True)

def _make_coroutine_wrapper(func, replace_callback):
    
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        future = TracebackFuture()
        ...

        try:
            result = func(*args, **kwargs)
        except (Return, StopIteration) as e:
            result = _value_from_stopiteration(e)
        except Exception:
            future.set_exc_info(sys.exc_info())
            return future
        else:
            if isinstance(result, GeneratorType):
                ...
                try:
                    orig_stack_contexts = stack_context._state.contexts
                    yielded = next(result)
                    if stack_context._state.contexts is not orig_stack_contexts:
                        yielded = TracebackFuture()
                        yielded.set_exception(
                            stack_context.StackContextInconsistentError(
                                'stack_context inconsistency (probably caused '
                                'by yield within a "with StackContext" block)'))
                except (StopIteration, Return) as e:
                    future.set_result(_value_from_stopiteration(e))
                except Exception:
                    future.set_exc_info(sys.exc_info())
                else:
                    Runner(result, future, yielded)
                try:
                    return future
                finally:
                    future = None



class Runner(object):

    def __init__(self, gen, result_future, first_yielded):
        self.gen = gen
        self.result_future = result_future
        self.future = _null_future
        self.yield_point = None
        self.pending_callbacks = None
        self.results = None
        self.running = False
        self.finished = False
        self.had_exception = False
        self.io_loop = IOLoop.current()
        # For efficiency, we do not create a stack context until we
        # reach a YieldPoint (stack contexts are required for the historical
        # semantics of YieldPoints, but not for Futures).  When we have
        # done so, this field will be set and must be called at the end
        # of the coroutine.
        self.stack_context_deactivate = None
        if self.handle_yield(first_yielded):
            self.run()

    def run(self):
        
        if self.running or self.finished:
            return
        try:
            self.running = True
            while True:
                future = self.future
                if not future.done():
                    return
                self.future = None
                try:
                    ...

                    try:
                        value = future.result()
                    except Exception:
                        self.had_exception = True
                        exc_info = sys.exc_info()

                    if exc_info is not None:
                        yielded = self.gen.throw(*exc_info)
                        exc_info = None
                    else:
                        yielded = self.gen.send(value)




See Also

  1. 쿠...sal: [컴][파이썬] tornado chat program



References

  1. Coroutines — Tornado 4.4.2 documentation
  2. tornado.gen — Simplify asynchronous code — Tornado 4.4.2 documentation


댓글 없음:

댓글 쓰기