맛동산

Tornado Docs를 읽어보자 : User's guide - Asynchronous and non-Blocking I/O 본문

파이썬/Tornado Framework

Tornado Docs를 읽어보자 : User's guide - Asynchronous and non-Blocking I/O

오지고지리고알파고포켓몬고 2017. 12. 16. 12:48

본 글은 tornado 학습 목적으로 의역으로 작성한 글이며, 오역이 있을 수 있음을 알려드리고 사실과 다른 내용이 발견될 때 마다 수정 작업을 수행할 예정입니다. 

기술적인 부분은 기본적인 사항 파악 후에 작성하도록 하겠습니다.


원문 - http://www.tornadoweb.org/en/stable/guide/async.html


Asynchronous and non-Blocking I/O

Real-time web features require a long-lived mostly-idle connection per user. In a traditional synchronous web server, this implies devoting one thread to each user, which can be very expensive.

To minimize the cost of concurrent connections, Tornado uses a single-threaded event loop. This means that all application code should aim to be asynchronous and non-blocking because only one operation can be active at a time.

The terms asynchronous and non-blocking are closely related and are often used interchangeably, but they are not quite the same thing.


Blocking

A function blocks when it waits for something to happen before returning. A function may block for many reasons: network I/O, disk I/O, mutexes, etc. In fact, every function blocks, at least a little bit, while it is running and using the CPU (for an extreme example that demonstrates why CPU blocking must be taken as seriously as other kinds of blocking, consider password hashing functions like bcrypt, which by design use hundreds of milliseconds of CPU time, far more than a typical network or disk access).

A function can be blocking in some respects and non-blocking in others. For example, tornado.httpclient in the default configuration blocks on DNS resolution but not on other network access (to mitigate this use ThreadedResolver or a tornado.curl_httpclient with a properly-configured build of libcurl). In the context of Tornado we generally talk about blocking in the context of network I/O, although all kinds of blocking are to be minimized.


Asynchronous

An asynchronous function returns before it is finished, and generally causes some work to happen in the background before triggering some future action in the application (as opposed to normalsynchronous functions, which do everything they are going to do before returning). There are many styles of asynchronous interfaces:

  • Callback argument
  • Return a placeholder (FuturePromiseDeferred)
  • Deliver to a queue
  • Callback registry (e.g. POSIX signals)

Regardless of which type of interface is used, asynchronous functions by definition interact differently with their callers; there is no free way to make a synchronous function asynchronous in a way that is transparent to its callers (systems like gevent use lightweight threads to offer performance comparable to asynchronous systems, but they do not actually make things asynchronous).


Examples

Here is a sample synchronous function:

from tornado.httpclient import HTTPClient

def synchronous_fetch(url):
    http_client = HTTPClient()
    response = http_client.fetch(url)
    return response.body

And here is the same function rewritten to be asynchronous with a callback argument:

from tornado.httpclient import AsyncHTTPClient

def asynchronous_fetch(url, callback):
    http_client = AsyncHTTPClient()
    def handle_response(response):
        callback(response.body)
    http_client.fetch(url, callback=handle_response)

And again with a Future instead of a callback:

from tornado.concurrent import Future

def async_fetch_future(url):
    http_client = AsyncHTTPClient()
    my_future = Future()
    fetch_future = http_client.fetch(url)
    fetch_future.add_done_callback(
        lambda f: my_future.set_result(f.result()))
    return my_future

The raw Future version is more complex, but Futures are nonetheless recommended practice in Tornado because they have two major advantages. Error handling is more consistent since theFuture.result method can simply raise an exception (as opposed to the ad-hoc error handling common in callback-oriented interfaces), and Futures lend themselves well to use with coroutines. Coroutines will be discussed in depth in the next section of this guide. Here is the coroutine version of our sample function, which is very similar to the original synchronous version:

from tornado import gen

@gen.coroutine
def fetch_coroutine(url):
    http_client = AsyncHTTPClient()
    response = yield http_client.fetch(url)
    raise gen.Return(response.body)

The statement raise gen.Return(response.body) is an artifact of Python 2, in which generators aren’t allowed to return values. To overcome this, Tornado coroutines raise a special kind of exception called a Return. The coroutine catches this exception and treats it like a returned value. In Python 3.3 and later, a return response.body achieves the same result.






Asynchronous and non-Blocking I/O(비동기와 논 블로킹 I/O)


실시간 기능을 사용하려면 유저당 연길이 오래동안 지속되어야 합니다. 전통적인 동기식(synchronous) 서버에서, 이는 사용자당 스레드를 제공해야 하는 것이며, 매우 비쌉니다.


연결 비용을 최소화 하기 위해, 토네이도는 싱글 스레드 event loop 사용합니다. 이것은 오직 작업만 활성화할 있기 때문에, 모든 어플리케이션 코드가 비동기(asynchronous) non-blocking 목표로 해야함을 의미합니다.


비동기와 non-blocking 밀접한 관련이 있으며 종종 같은 말로 쓰이지만, 그들은 같지 않습니다.


Blocking


return 일어나기 전까지 대기할 함수의 block 발생합니다. 함수는 여러가지 이유로 block됩니다 : 네트워크 I/O, 디스크 I/O, 뮤텍스, 기타 등등 이유로. 사실 모든 함수는 실행되는동안, cpu 사용하는동안 아주 조금씩은 block 있습니다.(CPU 블로킹이 다른 종류의 블로킹만큼 심각하게 받아 들여지는 이유를 보여주는 극단적인 예를 들면, bcrypt 같은 암호 해시 함수를 생각해봅시다. 이는 밀리 초의 CPU 시간을 사용하도록 설계 되어있으며, 일반적인 네트워크 또는 디스크 액세스보다 훨씬 많습니다.)


함수는 경우에 따라서 blocking되기도 non-blocking되기도 합니다. 예를들면, 기본 설정의 tornado.httpclient DNS 리솔루션에서 block되지만 다른 network access에선 그렇지 않습니다.(이것을 완화시키기 위해서 ThreadedResolver 또는 tornado.curl_httpclient 적절히 구성된 libcurl 함께 사용합니다.) 우리는 일반적으로 network I/O 주제로 이야기 했는데, 모든 blocking 최소화가 토네이도가 추구하는 입니다.


Asynchronous


비동기 함수는 종료되기 전에 return합니다, 그리고 일반적으로 응용 프로그램에서 향후 작업을 시작하기 전에 백그라운드에서 일부 작업이 발생합니다.(구글 번역) (반환하기 전에 모든일을 하는 보통 동기함수와 대조적으로) 다음은 많은 비동기 인터페이스 방법들 입니다 :

-> 동기(synchronous) 함수는 함수의 모든 작업이 끝난 뒤에 return 일어나고 비동기(asynchronous) 함수는 return 일어난 후에 백그라운드에서 작업이 수행된다는 내용입니다. 그래서 비동기 함수는 작업이 완료 됐을때 반환인자를 처리할 있는 callback함수를 함께 작성하곤 합니다.


  • Callback argument
  • Return a placeholder (FuturePromiseDeferred)
  • Deliver to a queue
  • Callback registry (e.g. POSIX signals)

사용되는 인터페이스에 상관없이, 비동기 함수는 호출자와 다르게 서로 상호 작용합니다; 호출자에게 투명한 방식으로 비동기식 동기 함수를 만드는 자유로운 방법은 없습니다.(gevent 같은 시스템은 경량 스레드를 사용하여 비동기 시스템에 필적하는 성능을 제공합니다. 하지만 실제로 비동기를 만들진 않습니다.)(구글 번역)


Example


다음은 동기 함수의 예입니다 :

from tornado.httpclient import HTTPClient

def synchronous_fetch(url):
    http_client = HTTPClient()
    response = http_client.fetch(url)
    return response.body

그리고 같은 함수를 콜백 인자와 비동기 방식으로 재작성 입니다 :

from tornado.httpclient import AsyncHTTPClient

def asynchronous_fetch(url, callback):
    http_client = AsyncHTTPClient()
    def handle_response(response):
        callback(response.body)
    http_client.fetch(url, callback=handle_response)

그리고 callback 대신 future :

from tornado.concurrent import Future

def async_fetch_future(url):
    http_client = AsyncHTTPClient()
    my_future = Future()
    fetch_future = http_client.fetch(url)
    fetch_future.add_done_callback(
        lambda f: my_future.set_result(f.result()))
    return my_future

raw한 버전의 future는 더 복잡합니다. 그럼에도 불구하고 2가지 이익이 있기 때문에 토네이도에서 future 연습해보길 추천합니다. Future.result 메소드는 단순 예외를 발생시킬 있기 때문에 오류처리에 일관성이 있습니다.(콜백 지향 인터페이스에서 흔히 발새하는 ad-hoc 오류 처리와 대조적입니다.) 그리고 코루틴과 궁합이 맞습니다. 코루틴은 가이드의 다음 장에서 깊게 다루겠습니다. 다음은 원래 동기 버전과 아주 유사한 코루틴 버전 샘플 함수입니다.

from tornado import gen

@gen.coroutine
def fetch_coroutine(url):
    http_client = AsyncHTTPClient()
    response = yield http_client.fetch(url)
    raise gen.Return(response.body)

Raise gen.Return(response.body) 파이썬2 구조이며, 생성자는 값을 반환할 없습니다. 이를 극복하기 위해, 토네이도 코루틴은 Return이라고 불리는 특별한 종류의 예외를 발생시킵니다. 코루틴은 예외를 캐치하고 반환 처럼 취급합니다. 파이썬 3.3 이후에, return response.body 이와 같은 결과를 얻습니다.


요약


토네이도는 싱글 스레드 event loop 사용

- Raise gen.Return(response.body) + exception called Return = return response.body


Comments