63.异步化.md 4.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
## 异步化

在前面的例子中,我们并没有对`RequestHandler`中的`get``post`方法进行异步处理,这就意味着,一旦在`get``post`方法中出现了耗时间的操作,不仅仅是当前请求被阻塞,按照Tornado框架的工作模式,其他的请求也会被阻塞,所以我们需要对耗时间的操作进行异步化处理。

在Tornado稍早一些的版本中,可以用装饰器实现请求方法的异步化或协程化来解决这个问题。

-`RequestHandler`的请求处理函数添加`@tornado.web.asynchronous`装饰器,如下所示:

  ```Python
  class AsyncReqHandler(RequestHandler):
      
      @tornado.web.asynchronous
      def get(self):
         http = httpclient.AsyncHTTPClient()
         http.fetch("http://example.com/", self._on_download)
  
      def _on_download(self, response):
         do_something_with_response(response)
         self.render("template.html")
  ```

-`RequestHandler`的请求处理函数添加`@tornado.gen.coroutine`装饰器,如下所示:

  ```Python
  class GenAsyncHandler(RequestHandler):
      
      @tornado.gen.coroutine
      def get(self):
          http_client = AsyncHTTPClient()
          response = yield http_client.fetch("http://example.com")
          do_something_with_response(response)
          self.render("template.html")
  ```

- 使用`@return_future`装饰器,如下所示:

  ```Python
  @return_future
  def future_func(arg1, arg2, callback):
      # Do stuff (possibly asynchronous)
      callback(result)
  
  async def caller():
      await future_func(arg1, arg2)
  ```

在Tornado 5.x版本中,这几个装饰器都被标记为**deprcated**(过时),我们可以通过Python 3.5中引入的`async``await`(在Python 3.7中已经成为正式的关键字)来达到同样的效果。当然,要实现异步化还得靠其他的支持异步操作的三方库来支持,如果请求处理函数中用到了不支持异步操作的三方库,就需要靠自己写包装类来支持异步化。

下面的代码演示了在读写数据库时如何实现请求处理的异步化。我们用到的数据库建表语句如下所示:

```SQL
create database hrs default charset utf8;

use hrs;

/* 创建部门表 */
create table tb_dept
(
    dno     int not null comment '部门编号',
    dname   varchar(10) not null comment '部门名称',
    dloc    varchar(20) not null comment '部门所在地',
    primary key (dno)
);

insert into tb_dept values
    (10, '会计部', '北京'),
    (20, '研发部', '成都'),
    (30, '销售部', '重庆'),
    (40, '运维部', '深圳');
```

我们通过下面的代码实现了查询和新增部门两个操作。

```Python
import json

import aiomysql
import tornado
import tornado.web

from tornado.ioloop import IOLoop
from tornado.options import define, parse_command_line, options

define('port', default=8000, type=int)


async def connect_mysql():
    return await aiomysql.connect(
        host='120.77.222.217',
        port=3306,
        db='hrs',
        user='root',
        password='123456',
    )


class HomeHandler(tornado.web.RequestHandler):

    async def get(self, no):
        async with self.settings['mysql'].cursor(aiomysql.DictCursor) as cursor:
            await cursor.execute("select * from tb_dept where dno=%s", (no, ))
            if cursor.rowcount == 0:
                self.finish(json.dumps({
                    'code': 20001,
                    'mesg': f'没有编号为{no}的部门'
                }))
                return
            row = await cursor.fetchone()
            self.finish(json.dumps(row))

    async def post(self, *args, **kwargs):
        no = self.get_argument('no')
        name = self.get_argument('name')
        loc = self.get_argument('loc')
        conn = self.settings['mysql']
        try:
            async with conn.cursor() as cursor:
                await cursor.execute('insert into tb_dept values (%s, %s, %s)',
                                     (no, name, loc))
            await conn.commit()
        except aiomysql.MySQLError:
            self.finish(json.dumps({
                'code': 20002,
                'mesg': '添加部门失败请确认部门信息'
            }))
        else:
            self.set_status(201)
            self.finish()


def make_app(config):
    return tornado.web.Application(
        handlers=[(r'/api/depts/(.*)', HomeHandler), ],
        **config
    )


def main():
    parse_command_line()
    app = make_app({
        'debug': True,
        'mysql': IOLoop.current().run_sync(connect_mysql)
    })
    app.listen(options.port)
    IOLoop.current().start()


if __name__ == '__main__':
    main()
```

上面的代码中,我们用到了`aiomysql`这个三方库,它基于`pymysql`封装,实现了对MySQL操作的异步化。操作Redis可以使用`aioredis`,访问MongoDB可以使用`motor`,这些都是支持异步操作的三方库。