Tornado 异步

参考资料

  • https://www.cnblogs.com/becker/p/9335136.html

Tornado依赖ioloop实现非阻塞式(同步多路IO复用)的请求响应,而ioloop采用Linux的Epoll异步网络IO模型实现。

Tornado的异步包括两个方面:异步服务器和异步客户端,无论服务器和客户端,具体的异步模式又分为回调callback和协程coroutine

  • Tornado服务端异步方式可以理解为一个Tornado的请求之内需要一个耗时性的任务,如果直接写在业务逻辑中可能会阻塞block整个服务。因此可以将任务放到异步中处理,实现异步的方式有两种:一种是yield挂起函数,一种是使用类线程池的方式。

  • Tornado客户端异步特性主要在于AsyncHTTPClient的使用,应用场景往往是在Tornado服务内需要针对另外的IO进行请求和处理。


异步yield

Tornado默认是单进程单线程,实时的Web特性通常需要为每个用户分配一个大部分时间都处于空闲状态的长连接,传统同步Web服务器中意味着需要给每个用户都分配一个专用的线路,这样的开销是十分巨大的。

为了减少对并发连接需要的开销,Tornado使用了一种单线程事件循环的方式,所有应用程序代码都必须是异步和非阻塞的,因为在同一时刻只有一个操作是有效的。

Tornado推荐使用协程来编写异步代码,协程使用Python关键字yield来替代链式回调来实现挂起和继续执行。

例如:服务器每接收到客户端传入20个字符以内的数据就反序回传,客户端用户若输入exit则退出。

$ vim server.py
#!/usr/bin/env python
# -*- conding: utf-8 -*-

from tornado.tcpserver import TCPServer
from tornado.ioloop import IOLoop
from tornado import iostream, gen

# 创建一个继承自TCPServer类的实例
class Server(TCPServer):
    @gen.coroutine
    # 如果客户端有连接过来,Tornado会创建一个iostream,然后调用handle_stream方法,调用时需传入iostream和client地址。
    def handle_stream(self, stream, address):
        try:
            while True:
                # 服务器没接收到20个字符以内的消息则反序会传
                msg = yield stream.read_bytes(20, partial=True)
                print(msg, address)
                stream.write(str(msg).encode("utf-8"))
                yield stream.write(msg[::-1])
                # 若服务器接收到exit字符串则断开连接
                if msg == "exit":
                    # 断开连接不用yield调用,无论双方谁断开连接,双方都会各自触发StreamClosedError异常。
                    stream.close()
        except iostream.StreamClosedError:
            pass

# 程序主入口
if __name__ == "__main__":
    # 创建一个继承自TCPServer内的实例
    server = Server()
    # 监听端口
    server.listen(9000)
    # 启动服务器
    server.start()
    # 启动消息循环 服务器开始运行
    IOLoop.current().start()
$ vim client.py
#!/usr/bin/env pythoon
# -*- coding: utf-8 -*-

from tornado.tcpclient import TCPClient
from tornado.ioloop import IOLoop
from tornado import iostream, gen

@gen.coroutine
def Trans():
    # 使用TCP客户端connect()方法连接服务器并返回iostream对象
    stream = yield TCPClient().connect("127.0.0.1", 9000)
    try:
        while True:
            # 客户端接收命令行输入
            data = input("enter: ")
            # 客户端向io流中写入数据并打印到屏幕
            yield stream.write(str(data).encode("utf-8"))
            # 客户端从io流中读取字节并打印到屏幕 
            back = yield stream.read_bytes(20, partial=True)
            print(back)
            msg = yield stream.read_bytes(20, partial=True)
            print(msg)
            # 若用户输入exit则退出循环
            if data == "exit":
                break
    except iostream.StreamClosedError:
        pass
# 程序主入口
if __name__ == "__main__":
    # run_sync函数中,Tornado会传启动消息循环并执行目标函数,之后结束消息循环。
    IOLoop.current().run_sync(Trans)

查看端口占用

# ss用于转储套接字统计信息
$ ss -lntpd | grep :9000
tcp    LISTEN   0        128               0.0.0.0:9000          0.0.0.0:*       users:(("python",pid=8824,fd=5))                                               
tcp    LISTEN   0        128                  [::]:9000             [::]:*       users:(("python",pid=8824,fd=6))  
# netstat显示网络连接、路由表、接口统计信息、防伪装连接、多播成员
$ netstat -tnlp | grep :9000
(并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户)
tcp        0      0 0.0.0.0:9000            0.0.0.0:*               LISTEN      8824/python         
tcp6       0      0 :::9000                 :::*                    LISTEN      8824/python   
# 列出系统上被进程打开的文件相关信息
$ lsof -i tcp:9000
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
python  8824   jc    5u  IPv4  66495      0t0  TCP *:9000 (LISTEN)
python  8824   jc    6u  IPv6  66496      0t0  TCP *:9000 (LISTEN)
# fuser显示当前程序在使用磁盘上的某个文件、挂载点、网络端口并给出程序进程PID。
$ fuser 9000/tcp
9000/tcp:             8824

运行测试

$ python server.py
b'hello world' ('127.0.0.1', 58564)
b'exit' ('127.0.0.1', 58564)

$ python client.py
enter: hello world
b"b'hello world'dlrow "
b'olleh'
enter: exit
b"b'exit'tixe"

例如:Tornado异步IO,一系列调用都通过回调函数实现。

$ vim server.py
#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from tornado.ioloop import IOLoop
from tornado.iostream import IOStream
import socket

def send_request():
    stream.write("GET /index.html HTTP/1.0\r\nHost:127.0.0.1\r\n\r\n")
    stream.read_util("\r\n\r\n", on_headers)

def on_headers(data):
    headers = {}
    for line in data.split("\r\n"):
        parts = line.split(":")
        if len(parts)==2 :
            headers[parts[0].strip()] = parts[1].strip()
    stream.read_bytes(int(headers["Content-Length"]), on_body)

def on_body(data):
    print(data)
    stream.close()
    IOLoop.instance().stop()

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
stream = IOStream(sock)
stream.connect(("127.0.0.1", 9000), send_request)
IOLoop.instance().start()

运行测试

$ python blocking.py

非阻塞状态

$ curl http://127.0.0.1:8001/index
index handler get

阻塞状态

$ curl http://127.0.0.1:8001/blocking
blocking 10 seconds

同步阻塞Blocking

什么是同步阻塞的IO网络模型呢?简单来说一个函数通常在其等待返回值的时候会被阻塞,被阻塞的理由有很多,比如网络IO、磁盘IO、互斥锁...

每个函数都会被阻塞,只是时间长短不同而已。当一个函数运行并占用CPU,一种极端的情况是将CPU阻塞的时间考虑在内,比如密码散列函数bcrypt它需要占据几百毫秒的CPU时间,这远远超过了通常对于网络和磁盘的请求时间。另一种情况是函数可以在某方面阻塞而在其他方面不阻塞,比如tornado.httpclient默认情况下将阻塞在DNS解析,而其他网络请求时则不会阻塞。在Tornado上下文中通常讨论论的是网络IO上下文阻塞。

例如:

$ vim blocking.py
!/usr/bin/env python
# -*- coding: utf-8 -*-

from tornado.web import RequestHandler, Application
from tornado.ioloop import IOLoop
import time

class IndexHandler(RequestHandler):
    def get(self):
        self.write("index handler get")

def doing():
    time.sleep(10)
    return "blocking 10 seconds"

class BlockingHandler(RequestHandler):
    def get(self):
        result = doing()
        self.write(result)

app = Application([
    (r"/index", IndexHandler),
    (r"/blocking", BlockingHandler)
])

if __name__ == "__main__":
    app.listen(8001)
    IOLoop.instance().start()

异步非阻塞Non Blocking

异步函数在其结束前就已经返回了,通常在程序中触发一些动作后在后台执行一些任务,而同步函数是需要在返回前做完所有的事情。

常见异步接口的类型

  • 回调函数 基本不用
  • Tornado协程 + 生成器gen
  • Tornado协程 + Future
  • 线程池进程池

协程

什么是协程Coroutine呢?

Coroutines are computer program components that generalize subroutines for nonpreemptive multitasking by allowing multiple entry points for suspending and resuming executionat certain locations.

协程在编程过程中更加习惯性称为子例程subroutine,通俗叫做也就是函数或者是过程。

子例程与协程的异同点在于

  • 子例程subroutine只是一个入口,也就是函数调用,将实参传递给形参并开始执行。
  • 子例程subroutine只有一个出口,也就是函数返回return,当函数执行完毕或引发异常时,将控制权转移给调用者。
  • 协程coroutine是子例程基础上一种更加宽泛定义的计算机程序模块,子例程可以看作是协程的特例。
  • 协程coroutine可以拥有多个入口点,允许从一个入口点执行到下一个入口点之前暂停以及保存执行状态,等到合适的时机恢复执行状态,并从下一个入口点重新开始执行。

线性模块与协程模块的异同点

协程代码块表示一个入口点和下一个入口点(或者是退出点)中的代码

  • 线性模块是指一个同步函数的函数体是线性执行的,也就是说一个模块中的每一行代码都会相续执行。一个模块在执行过程中,如果没有执行完毕就退出,是不会去执行其他模块中的代码。

  • 协程模块由多个入口点的代码和多个协程代码块共同组成,第一个入口点通常是一个函数入口点,其组织形式如:函数入口点->协程代码块->入口点->协程代码块...

一个协程模块如果只含有单一入口点和单一协程代码块,假设这个协程代码块全部都是同步代码,当然这个协程模块是一个线性执行模块,但是如果含有多个入口点和多个协程代码块,那就不是一个线性模块。执行一个协程模块过程实际上是分散的,也就说在不同的时间段执行不同的协程代码块,协程代码块的执行时间段是彼此不相交的。但也是顺序的即后一个协程代码块在前一个协程代码块执行结束后才执行。

线性模块与协程模块属于同一协程模块的相续协程代码块的中间时间间隙,也可能有很多其他协程模块的协程代码片段在执行。

生成器gen

什么是生成器呢?简单来说一边做循环一边做计算的就叫做生成器。Python中使用带yield关键字的函数就是生成器。

谈到协程必须要理解Python语义中的生成器generator,在pep255中提到simple generatoryield语句(注意这里还不是yield表达式)的实现中,一个basic idea是提供一种函数,能够返回中间结果给调用者,然后维护函数的局部状态,以便函数离开后能够恢复执行。

例如:获取斐波拉契数列

$ vim fib.py
#!/usr/bin/env python3
# -*- coding:utf-8 -*-

# 斐波拉契数列计算
def fib(max):
    a, b = 0, 1
    while(b < max):
        yield b
        total = a + b
        a = b
        b = total

result = fib(10)
print(list(result))
$ python3 fib.py
[1, 1, 2, 3, 5, 8]

例如:迭代器

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

# 生成下一个斐波拉契数列,类似列表的迭代器。
def fib():
    # 变量初始化:a初始化为0,b初始化为1
    a, b = 0, 1
    while 1:
        # 当yield b开始执行时1会被返回给调用者
        yield b # 含有yield表达式的函数也就是生成器
        # 当fib函数恢复执行时a会被赋值为1,此时b也是1,然后b再将1返回给调用者,如此往返循环。
        a, b = b, a+b

# 调用者
def caller():
    for num in fib():
        print(num)

# 运行测试
caller()

生成器是一个含有yield表达式的函数,此时该函数又叫做生成器。一个生成器永远都是异步的,即使生成器模块中含有阻塞代码。因为调用一个生成器时,生成器的参数会绑定到生成器,结果返回的是一个类型为types.GeneratorType的生成器对象,生成器对象不会去执行生成器模块中的代码。

当每次调用一个GeneratorType对象的next()方法时,生成器函数将会执行到下一个yield语句,或者是碰到一个return语句,亦或者是执行到生成器函数结束。

在pep324中对Generator做了进一步加强,增加了GeneratorTypesend()方法和yield表达式语义。

yield表达式可以用作为等号右边的表达式,如果对Generator调用send(None)方法,生成器函数会从一开始执行到yield表达式。那么下一次对Generator调用send(argument),Generator将会恢复执行。可以在生成器函数体内获取这个argument,这个argument将会作为yiled表达式的返回值。

从以上来看,Generator已经具备了协程的部分能力,如能够暂停执行、保存状态、能够恢复执行、能够异步执行。但是此时Generator还不是一个协程。一个真正的协程能够控制代码在什么时候会继续执行,而一个Generator执行遇到一个yield表达式或语句时会将执行控制权转交给调用者。

Hower, it is still possible to implement coroutines on top of a generator facility, with the aid of a top-level dispatcher routine(a trampoline, essentially) that passes control explicitly to child generators identified by tokens passed back from the generator.

也可以实现一个顶级的调度子例程,将执行控制权转移回Generator,从而让其继续执行。在Tornado中ioloop就是这样的顶级调度子例程,每个协程模块通过函数装饰器coroutineioloop进行通信,从而ioloop可以在协程模块执行暂停后,再在合适的时机重新调度协程模块运行。

gen.corouotine 装饰器

  • Tornado的异步特性必须使用异步库,否则单个进程会阻塞,因此是达不到异步效果。
  • Tornado异步库中最常见的是asynHTTPClient
  • gen.coroutine装饰器可以让本来依靠回调函数的异步编程看起来跟同步编程一样

Tornado中的协程是基于Python语言的Generator并结合一个全局的调度器IOLoop实现的,Generator通过函数装饰器@gen.coroutine和IOLoop进行通信。

IOLoop并没有直接的控制能力去调度恢复被暂停的协程继续执行。由于Future对象在协程中被yield导致协程暂停,IOLoop只能调度另外一个代码模块执行,而在这个执行的代码模块中刚好可以访问到这个Future对象并可以将其set_result。结果是通过IOLoop间接地恢复暂停的协程去执行。在不同的执行代码模块中,会共享Future对象,通过彼此合作,协程调度得以顺序执行。从某种意义上来讲,Future对象类似于Window中的Event内核对象,Window中的Event主要是用于线程中的同步。而协程中的yield future相当于WaitForSingleObject(event_object),协程中的future.set_result(result)则相当于SetEvent(event_object。因此Future和Event的不同点在于协亨是借助Future来恢复执行,而线程则是借助于Event会进行线程间通信的。

函数装饰器本质上是也是一个函数,又称为这个函数为装饰器函数。装饰器函数签名包含一个函数对象参数,它可以调用对象的callback回调函数,返回的结果是一个装饰器内部定义的新函数对象。如果返回的函数对象被调用,装饰器函数的参数即函数对象也会被调用。不过,会在这个装饰器函数参数调用前做一些事情,或者在这个装饰器函数参数调用后做一些事情。实际上做的这些事情,就是利用内部自定义的函数对象对参数(原函数)的一些装饰(额外操作)。

当一个函数被装饰器修饰后,在后续函数调用时实际上调用的是装饰器函数返回的内部函数对象。Tornado中的协程coroutine是通过tornado.gen中的coroutine装饰器实现的,对于Tornado中是如何使用@gen.coroutine修饰函数的,主要需要理解coroutine装饰器函数内部定义的新函数对象内部的行为。

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

Tornado协程 + 生成器gen

  • 包含了yield关键字的函数是一个生成器generator,所有的生成器都是异步的,当调用它们的时候会返回一个生成器对象,而不是一个执行完毕的结果。

  • @gen.coroutine装饰器通过yield表达式和生成器进行交流,通过返回一个Future与协程的调用方进行交互。

  • @gen.coroutine让函数以异步协程的方式运行,但必须依赖第三方异步库,而且要求函数本身不是阻塞blocking的。比如os.sleep()方法是阻塞的blocking的,因此是没有办法实现异步非阻塞的。

  • 协程一般不会抛出异常,它们抛出的任何异常都将被Future捕获直到被得到,这意味着使用正确的方式调用协程是非常重要的,否则可能有被忽略的错误。

例如:

$ vim generator.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-

from tornado.web import Application, RequestHandler
from tornado.ioloop import IOLoop
from tornado import gen
import time

class IndexHandler(RequestHandler):
    def get(self):
        self.write("index handler get")

# 使用@gen.cortinue装饰器后最终结果将返回一个可以被yield挂起的生成器Future对象
# 生成器Future对象需要使用raise gen.Return方法返回
@gen.coroutine
def doing():
    # time.sleep函数是阻塞的,但不支持异步。
    # time.sleep(5)
    # gen.sleep函数可用来模拟IO等待
    yield gen.sleep(5)
    raise gen.Return("non blocking")

class NonBlockingHandler(RequestHandler):
    @gen.coroutine
    def get(self):
        result = yield doing()
        self.write(result)

app = Application([
    (r"/index", IndexHandler),
    (r"/nonblocking", NonBlockingHandler)
])

if __name__ == "__main__":
    app.listen(8002)
    IOLoop.instance().start()

运行测试

$ python generator.py

非阻塞状态

$ curl http://127.0.0.1:82/index
index handler get

阻塞状态

$ curl http://127.0.0.1:82/nonblocking
non blocking

Future

什么是Future呢?

Future类位于Tornado源码的concurrent模块,Future是Tornado实现异步的一个重要对象,Future的工作如何它的名字一样,是用来装载“未来”的,也就是还没有完成操作的结果,即异步操作结果的占位符。

简单来说,Future是一个异步操作的占位符

  • Future是一个对象
  • Future对象包含_result_callback属性,用于存储异步操作的结果以及回调。
  • Future对象包含add_done_callback()添加回调函数的方法、set_result()设置异步操作结果的方法。
  • Future对象中对应的异步操作完毕后Future对象会被set_done,然后遍历并运行_callbacks中的回调函数。

Futures的优势在于错误处理时可通过Future.result函数简单的抛出异常,不同于传统基于回调接口的一对一的错误处理方式。另外Future对携程兼容的很好。

Tornado内置的Future(tornado.concurrent.Future)futures包中的Future(concurrent.futures.Future)相似,不过Tornado内置的Future不是线程安全的,因为Tornado本身是单线程的。

Future的使用

# 在IOLoop中注册Future
tornado.ioloop.IOLoop.add_future(future, future_done_callback_func)
# 在@gen.coroutine内部结合yield使用
@gen.coroutine
def fn():
  result = yield future

Future的方法

class Future(object):
  # 返回future的值future._result,如果又执行异常那么将会触发异常。
  def result(self, timeout=None):
    self._clear_tb_log()
    if self._result is not None:
      return self._result
    if self._exec_info is not None:
      raise_exc_info(self._exc_info)
    self._check_done()
    return self._result 
  # add_done_callback: 为future添加回调到回调列表中,在set_result后执行,不过如果future已经完成则会直接执行这个回调,不放入回调列表。
  def add_done_callback(self, fn):
    if self._done:
      fn(self)
    else:
      self._callbacks.append(fn)
  # set_result:为future设置值然后执行回调列表中的所有回调,回调传入的唯一参数就是future本身。
  def set_result(self, result):
    self._result = result
    self._set_done()
  • def done(self):
    Future的_result成员是否被设置
  • def result(self, timeout=None):
    获取Future对象的结果
  • def add_done_callback(self, fn):
    添加一个回调函数fn给Future对象,如果这个Future对象已经done则直接执行fn,否则将fn加入到Future类的成员列表中保存。
  • def _set_done(self):
    内部函数用于遍历列表,逐个调用列表中的callback回调函数,也就是add_done_callback中添加的。
  • def set_result(self, result):
    给Future对象设置result并调用_set_done,当Future对象获得result后所有的add_done_callback加入的回调函数就会执行。

例如:使用@gen.coroutine实现异步HTTP请求

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from tornado.web import RequestHandler
from tornado.httpclient import AsyncHTTPClient

class AsyncFetch(RequestHandler):
    @gen.coroutine
    def get(self, *args, **kwargs):
        address = "http://www.baidu.com"
        client = AsyncHTTPClient(address, request_timeout=2)
        self.finish(response.body)

例如:使用Future实现的异步HTTP请求

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from tornado.web import RequestHandler
from tornado.httpclient import AsyncHTTPClient
from tornado.ioloop import IOLoop

class AsyncFetch(RequestHandler):
    @asynchronous
    def get(self, *args, **kwargs):
        client = AsyncHTTPClient()
        future = client.fetch(address, request_timeout=2)
        IOLoop.current().add_futures(future, callback=self.on_response)
    
    def on_response(self, future):
        response = future.result()
        self.finish(response.body)

Tornado协程 + Future

例如:

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from tornado.web import RequestHandler, Application
from tornado.ioloop import IOLoop
from tornado.concurrent import Future

class IndexHandler(RequestHandler):
    def get(self):
        self.write("index handler get method")

def doing():
    future = Future()
    future.set_result("non blocking")
    return future

class NonBlockingHandler(RequestHandler):
    @gen.coroutine
    def get(self):
        result = yield doing()
        self.write(result)

app = Application([
    (r"/index", IndexHandler),
    (r"/nonblocking", NonBlockingHandler)
])

if __name__ == "__main__":
    app.listen(8000)
    IOLoop.instance().start()

例如:装饰器 + Future实现Tornado异步非阻塞

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from tornado.web import Application, RequestHandler
from tornado.ioloop import IOLoop
from tornado import gen

future = None

class IndexHandler(RequestHandler):
    def get(self):
        global future
        future.set_result(None)
        self.write("index handler get method")

class MainHandler(RequestHandler):
    @gen.coroutine
    def get(self):
        global future
        future = Future()
        future.add_done_callback(self.done)
        yield future
    def done(self, *args, **kwargs):
        self.write("main handler done method")
        self.finish()

application = Application([
    (r"/index", IndexHandler),
    (r"/main", MainHandler)
])

if __name__ == "__main__":
    application.listen(8000)
    IOLoop.instance().start()

当向MainHandler发送HTTP的GET请求时,由于get()方法被@gen.coroutine装饰且yield了一个Future对象,所以Tornado会等待,等待用户向future对象中放置数据或发送信号,如果获取到数据或信号之后,就会开始执行done()方法。这里需要注意的是,在等待用户向future对象中放置数据或信号时,连接时不断开的。

线程池

协程coroutine是给非阻塞Non-blocking函数提供异步协程的方式运行,ThreadPoolExecutor线程池执行器则可以给阻塞blocking的函数提供异步的方式运行,但是由于是多线程的,Python使用多线程对性能来说是需要谨慎的,大量的计算量的情况可能会造成性能下降。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import time
from tornado import  gen
from tornado.concurrent import run_on_executor
from concurrent.futures import ThreadPoolExecutor
from tornado.web import Application, RequestHandler
from tornado.ioloop import IOLoop

class IndexHandler(RequestHandler):
    def get(self):
        self.write("index handler get method")
        self.write("index handler write message")
        print("index handler")

class NonBlockingHandler(RequestHandler):
    exector =  ThreadPoolExecutor(4)
    @gen.coroutine
    def get(self):
        result = yield self.doing()
        self.write(result)
        print(result)
    # 使用Tornado线程池不需要添加装饰器到IO函数中
    @run_on_executor
    def doing(self):
        # 模拟同步阻塞
        #time.sleep(10)
        # 模拟异步阻塞
        #yield gen.sleep(10)
        # 模拟IO任务
        os.system("pint -c 10 www.baidu.com")
        return "non blocking doing"

application = Application([
    (r"/index", IndexHandler),
    (r"/nonblocking", NonBlockingHandler)
])

if __name__ == "__main__":
    application.listen(8000)
    IOLoop.instance().start()

你可能感兴趣的:(Tornado 异步)