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 을 사용할 때는 아래와 같은 흐름을 갖는다.- result = func(*args, **kwargs) --> fetch_coroutine
- yield --> result = func(*args, **kwargs) 로 돌아온다.
- yielded = next(result)
- Runner(result, future, yielded)
- Runner.run() 에서 일을 마치면, send() 를 해준다.
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
References
- Coroutines — Tornado 4.4.2 documentation
- tornado.gen — Simplify asynchronous code — Tornado 4.4.2 documentation
댓글 없음:
댓글 쓰기