文章目录
  1. 1. 正文
  2. 2. 更新

正文

自从Python 3.5之后允许async/await关键字来定义原生协程之后,aiohttp等基于asyncio标准库的异步库就在蓬勃发展。
tornado也对async/await关键字定义的原生协程做了一定的兼容。
由于tornadohttpclient功能不太强大(比如不支持cookie保持回话等)。
所以我打算使用aiohttp里面的ClientSession来做HTTP客户端。

不过在使用的过程中发现在tornado中使用aiohttp的会有些问题。

示例如下:

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
#!/usr/bin/env python3
# coding: utf-8

import asyncio

import aiohttp
import tornado.web
from tornado.platform.asyncio import AsyncIOMainLoop

session = aiohttp.ClientSession()


class MainHandler(tornado.web.RequestHandler):

async def get(self):
async with session.get("http://baidu.com") as res:
text = await res.text()
self.write(text)

pass


if __name__ == "__main__":
AsyncIOMainLoop().install() # 使用asyncio的事件循环
app = tornado.web.Application([(r"/", MainHandler)])
app.listen(8888)
asyncio.get_event_loop().run_forever()

下面是访问/捕获到的输出和异常信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Uncaught exception GET / (127.0.0.1)
HTTPServerRequest(protocol='http', host='localhost:8888', method='GET', uri='/', version='HTTP/1.1', remote_ip='127.0.0.1', headers={'Accept': '*/*', 'Host': 'localhost:8888', 'User-Agent': 'curl/7.50.1'})
Traceback (most recent call last):
File "/usr/local/lib/python3.5/dist-packages/tornado/web.py", line 1469, in _execute
result = yield result
File "/usr/local/lib/python3.5/dist-packages/tornado/gen.py", line 1015, in run
value = future.result()
File "/usr/local/lib/python3.5/dist-packages/tornado/concurrent.py", line 237, in result
raise_exc_info(self._exc_info)
File "<string>", line 3, in raise_exc_info
File "/usr/local/lib/python3.5/dist-packages/tornado/gen.py", line 285, in wrapper
yielded = next(result)
File "<string>", line 6, in _wrap_awaitable
File "/home/jzqt/Code/Python/1.py", line 16, in get
async with session.get("http://baidu.com") as res:
File "/usr/local/lib/python3.5/dist-packages/aiohttp/client.py", line 540, in __aenter__
self._resp = yield from self._coro
File "/usr/local/lib/python3.5/dist-packages/aiohttp/client.py", line 175, in _request
with Timeout(timeout, loop=self._loop):
File "/usr/local/lib/python3.5/dist-packages/async_timeout/__init__.py", line 33, in __enter__
raise RuntimeError('Timeout context manager should be used '
RuntimeError: Timeout context manager should be used inside a task
500 GET / (127.0.0.1) 34.12ms

关于这个问题其实在Github上面以及有人提到了。

tornado的作者也给出了对应的解决办法和回答,因为目前tornadoasyncio并不能互相兼容各自的协程,但是对于原生协程还是都可以使用的。
由于aiohttp里面的库依赖于asyncio里面的处理逻辑,所以导致这个问题发生。
对此tornado作者给出的解决方案也是使用推荐的tornado系列的HTTP客户端以及协程等相关的异步工具。

可惜asyncioioloop没有像tornado一样的run_sync()方法,否则就可以实现将原生协程直接送给asyncio的事件循环来调度。

在此我找到了一个解决办法,那就是将原生协程用asyncio.Task给封装起来,然后直接await,不会出现以上的错误,比如:

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
#!/usr/bin/env python3
# coding: utf-8

import asyncio

import aiohttp
import tornado.web
from tornado.platform.asyncio import AsyncIOMainLoop

session = aiohttp.ClientSession()


async def coro():
async with session.get("http://baidu.com") as res:
text = await res.text()
return text


class MainHandler(tornado.web.RequestHandler):

async def get(self):
text = await asyncio.get_event_loop().create_task(coro())
self.write(text)

pass


if __name__ == "__main__":
AsyncIOMainLoop().install()
app = tornado.web.Application([(r"/", MainHandler)])
app.listen(8888)
asyncio.get_event_loop().run_forever()

这样解决的话代码还是有点丑陋,其实可以写成一个装饰器来完成这部分的兼容,代码如下:

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
#!/usr/bin/env python3
# coding: utf-8

import asyncio
import functools

import aiohttp
import tornado.web
from tornado.platform.asyncio import AsyncIOMainLoop

session = aiohttp.ClientSession()


def convert_asyncio_task(method):
@functools.wraps(method)
async def wrapper(self, *args, **kwargs):
coro = method(self, *args, **kwargs)
return await asyncio.get_event_loop().create_task(coro)
return wrapper


class MainHandler(tornado.web.RequestHandler):

@convert_asyncio_task
async def get(self):
async with session.get("http://baidu.com") as res:
text = await res.text()
self.write(text)

pass


if __name__ == "__main__":
AsyncIOMainLoop().install()
app = tornado.web.Application([(r"/", MainHandler)])
app.listen(8888)
asyncio.get_event_loop().run_forever()

这么解决就比较完美了,等到tornado可以无缝兼容asyncio的协程以及aiohttp的HTTP客户端操作的时候再将装饰器去掉即可。

更新

之前的解决方案其实不太干净,正确的方式不是将协程转换为Task,我认为转换为Future才是更符合的,新的装饰器代码如下

1
2
3
4
5
6
def convert_asyncio_task(method):
@functools.wraps(method)
async def wrapper(self, *args, **kwargs):
coro = method(self, *args, **kwargs)
return await asyncio.ensure_future(coro)
return wrapper

这样还避免了之前的解决方案中限定了ioloop的问题。

文章目录
  1. 1. 正文
  2. 2. 更新