为什么要用异步
一般代码的同步执行
同步和异步通常用来形容一次方法调用。
-
同步方法
调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
每次只能向目标服务器发送一个请求,待其返回数据后才能进行下一次请求,若请求较多的情况下易发生阻塞。
-
异步方法
调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而异步方法通常会在另外一个线程中执行着。整个过程,不会阻碍调用者的工作。
对于调用者来说,异步调用似乎是在一瞬间完成的。如果异步调用需要返回结果,那么当异步调用真实完成时,则会通知调用者。
可同时发送多个请求到目标服务器,较早返回数据的将会被优先处理
举个例子
打个比方,比如我们去购物,如果你去商场实体店买一台空调,当你到了商场看中了一款空调,你就想售货员下单。售货员去仓库帮你调配物品。这天你热的实在不行了。就催着商家赶紧给你配送,于是你就等在商场里,候着他们,直到商家把你和空调一起送回家,一次愉快的购物就结束了。这就是同步调用。
不过,如果我们赶时髦,就坐再家里打开电脑,在网上订购了一台空调。当你完成网上支付的时候,对你来说购物过程已经结束了。虽然空调还没有送到家,但是你的任务都已经完成了。商家接到你的订单后,就会加紧安排送货,当然这一切已经跟你无关了,你已经支付完成,想什么就能去干什么了,出去溜达几圈都不成问题。等送货上门的时候,接到商家电话,回家一趟签收即可。这就是异步调用。
异步解决的问题:
异步处理可以让应用在长时间的API和数据库请求中避免阻塞的时间耗费,最终更快地服务更多请求
如果client请求server处理的handler里面有一个阻塞的耗时操作,那么整体的server性能就会下降。
比如: 访问一个耗时的网站请求 www.douban.com/search, 这个结果要在5秒后才返回值。
当我访问的话,肯定是要等5秒钟,这时候,要是有别的客户要连接的别的页面,(不堵塞的页面)
你猜他能马上显示吗?不能的。。。 他也是要等当前这个5秒延迟过后,才能访问的。
幸运的是,tornado提供了一套异步机制,方便我们实现自己的异步操作。
当handler处理需要进行其余的网络操作的时候,tornado提供了一个AsyncHTTPClient来支持异步。
注意
异步代码增加了复杂度,只在特定场景使用
应用异步
先从同步版本开始
- 增加一个保存 URL 图片的功能
- handler 和路由
- 使用协程 coroutine
tornado 的异步模块
- tornado.gen.coroutine + yield
- tornado.httpclient.AsyncHTTPClient
更多说明文档 异步和非阻塞I/O — Tornado 4.3 文档
tornado.httpclient.HTTPClient()
tornado内置的HTTP客户端对象 ( 阻塞 )
后端同步操作,服务器通过内置的客户端对象,抓取目标url地址的数据,返回给前端页面,通过dom操作渲染页面
http_client = httpclient.HTTPClient()
try:
response = http_client.fetch("http://www.google.com/")
print response.body
except httpclient.HTTPError as e:
print("Error: " + str(e))
except Exception as e:
print("Error: " + str(e))
http_client.close()
close()
关闭该 HTTPClient, 释放所有使用的资源.
fetch()
执行一个请求, 返回一个 HTTPResponse
对象.
code
class URLSaveHandler(AuthBaseHandler):
"""保存指定url的图片 同步方法"""
@tornado.web.authenticated
def get(self, *args, **kwargs):
url = self.get_argument('url', None)
response = self.fetch_image(url) # 获取指定url的图片
if not response.body: # 数据被封装在响应对象的body属性中
self.write('empty data')
return
image_saver = ImageSave(self.settings['static_path'], 'x.jpg')
image_saver.save_image(response.body) # body 就是图片数据 保存图片
image_saver.make_thumbs() # 做缩略图
# 添加到数据库,拿到 post 实例
post = Posts.add_post_for(self.current_user, image_saver.image_url, image_saver.thumb_url)
print("-- {} -end fetch:#{}".format(datetime.now(), post.id))
self.redirect('/post/{}'.format(post.id)) # 跳转到 post 页面
def fetch_image(self, url):
"""获取指定url的图片"""
client = tornado.httpclient.HTTPClient() # 获取同步操作对象
print("-- {} -going to fetch:{}".format(datetime.now(), url))
response = client.fetch(url) # 获取url对应的内容 得到响应对象
return response
tornado.httpclient.AsyncHTTPClient
tornado内置的HTTP客户端对象的异步操作对象 (非阻塞 )
def handle_request(response):
if response.error:
print "Error:", response.error
else:
print response.body
http_client = AsyncHTTPClient()
http_client.fetch("http://www.google.com/", handle_request)
code
class AsyncURLSaveHandler(AuthBaseHandler):
"""保存指定url的图片 异步方法"""
@tornado.web.authenticated
@tornado.gen.coroutine
def get(self, *args, **kwargs):
url = self.get_argument('url', None)
response = yield self.fetch_image(url) # 获取指定url的图片
if not response.body: # 数据被封装在响应对象的body属性中
self.write('empty data')
return
image_saver = ImageSave(self.settings['static_path'], 'x.jpg')
image_saver.save_image(response.body) # 保存图片
image_saver.make_thumbs() # 缩略图
post = Posts.add_post_for(self.current_user, image_saver.image_url, image_saver.thumb_url) # 添加到数据库
print("-- {} -end fetch:#{}".format(datetime.now(), post.id))
self.redirect('/post/{}'.format(post.id))
@tornado.gen.coroutine
def fetch_image(self, url):
"""获取指定url的图片"""
client = tornado.httpclient.AsyncHTTPClient() # 获取异步操作对象
print("-- {} -going to fetch:{}".format(datetime.now(), url))
yield tornado.gen.sleep(6)
response = yield client.fetch(url) # 获取url对应的内容 得到响应对象
return response
coroutine 装饰器
指定改请求为协程模式,说明白点就是能使用 yield 配合 Tornado 编写异步程序。
from tornado import gen
@gen.coroutine
def fetch_coroutine(url):
client = AsyncHTTPClient()
response = yield client.fetch(url)
return response.body
@gen.coroutine
此装饰器代表的是协程, 与关键字yield
搭配使用client.fetch(url)
请求网络是耗时操作, 通过关键字yield
来挂起调用, 而当client.fetch(url)
请求完成时再继续从函数挂起的位置继续往下执行.
协程模块tornado.gen
tornado.gen
是根据生成器(generator)实现的,用来更加简单的实现异步。
tornado.gen.coroutine
的实现思路:
generator中的yield
语句可以使函数暂停执行,而send()
方法则可以恢复函数的执行。
tornado将那些异步操作(fetch()
)放置到yield语句后,当这些异步操作完成后,tornado会将结果send()
至generator中恢复函数执行。
在tornado中大多数的异步操作返回一个Future
对象
yield Future
对象 会 返回该异步操作的结果,这句话的意思就是说 假如 response = yield some_future_obj
当some_future_obj
所对应的异步操作完成后会自动的将该异步操作的结果赋值给 response
Response 对象
class tornado.httpclient.HTTPResponse()
HTTP 响应对象
属性:
request: HTTPRequest 对象
body: string 化的响应体 (从 self.buffer
的需求创建)
https://www.cnblogs.com/Erick-L/p/7068112.html
协程详解:
同步异步I/O客户端
import tornado.httpclient
def ssync_visit():
client = tornado.httpclient.HTTPClient() # 获取同步操作对象
# 获取url对应的内容 得到响应对象
response = client.fetch('www.baidu.com') # 阻塞,直到网站请求完成
print(response.body)
def async_visit():
client = tornado.httpclient.AsyncHTTPClient() # 获取异步操作对象
response = yield client.fetch('www.baidu.com') # 非阻塞
print(response.body)
协程
编写协程函数
import tornado.httpclient
from tornado import gen # 引入协程库
@tornado.gen.coroutine
def coroutine_visit():
client = tornado.httpclient.AsyncHTTPClient() # 获取异步操作对象
response = yield client.fetch('www.baidu.com') # 非阻塞
print(response.body)
调用协程函数
由于Tornado协程基于python的yield关键字实现,所以不能调用普通函数一样调用协程函数
协程函数可通过以下三种方式调用
-
在本身是协程的函数内通过yield关键字调用
-
在IOLoop尚未启动时,通过IOLoop的run_sync()函数调用
-
在IOLoop已经启动时,通过IOLoop的spawn_callback()函数调用
在本身是协程的函数内通过yield关键字调用
下面是一个通过协程函数调用协程函数的例子
@gen.coroutine
def outer_coroutine():
print('开始调用另一个协程')
yield coroutine_visit()
print('outer_coroutine 调用结束')
outer_coroutine和coroutine_visit都是协程函数,他们之间可以通过yield关键字进行调用
@tornado.gen.coroutine
def get(self, *args, **kwargs):
url = self.get_argument('url', None)
response = yield self.fetch_image(url)
print(response.body)
@tornado.gen.coroutine
def fetch_image(self, url):
"""获取指定url的图片"""
client = tornado.httpclient.AsyncHTTPClient()
response = yield client.fetch(url)
return response
get和fetch_image都是协程函数,他们之间可以通过yield关键字进行调用
在IOLoop尚未启动时,通过IOLoop的run_sync()函数调用
IOLoop 是Tornado的主事件循环对象,Tornado程序通过它监听外部客户端的访问请求,并执行相应的操作,当程序尚未进入IOLoop的runing状态时,可以通过run_sync()函数调用协程函数,比如:
from tornado import gen # 引入协程库
from tornado.ioloop import IOLoop
from tornado.httpclient import AsyncHTTPClient
@tornado.gen.coroutine
def coroutine_visit():
client = tornado.httpclient.AsyncHTTPClient()
response = yield client.fetch('http://www.baidu.com/')
print(response.body)
def func_normal():
print('开始调用协程')
IOLoop.current().run_sync(lambda: coroutine_visit())
print('结束协程调用')
func_normal()
本例中run_sync()函数将当前函数的执行进行阻塞,直到被调用的协程执行完成
Tornado 要求协程函数在IOloop的running状态才能被调用,只不过run_sync函数自动完成了启动,停止IOLoop的步骤,他的实现逻辑为:启动IOLoop-调用被lambda封装的协程函数-停止IOLoop
在IOLoop已经启动时,通过IOLoop的spawn_callback()函数调用
from tornado import gen # 引入协程库
from tornado.ioloop import IOLoop
from tornado.httpclient import AsyncHTTPClient
@tornado.gen.coroutine
def coroutine_visit():
client = tornado.httpclient.AsyncHTTPClient()
response = yield client.fetch('http://www.baidu.com/')
print(response.body)
def func_normal():
print('开始调用协程')
IOLoop.current().spawn_callback(coroutine_visit)
print('结束协程调用')
func_normal()
本例中spawn_callback函数不会等待被调用的协程执行完成,而协程函数将会由IOLoop在合适的时机进行调用,并且spawn_callback函数没有提供返回值的方法,所以只能用该函数调用没有返回值的协程函数
tornado 协程结合异步
import tornado.web
import tornado.httpclient
class AsyncURLSaveHandler(tornado.web.RequestHandler):
@tornado.gen.coroutine
def get(self):
http = tornado.httpclient.AsyncHTTPClient()
response = yield http.fetch('http://www.baidu.com')
self.write(response.body)
用tornado.gen.coroutine装饰AsyncURLSaveHandler的get(),post()函数
使用异步对象处理耗时操作,
调用yield关键字获取异步对象的处理结果
作业
增加 /save 的 handler,实现异步的功能
请求随机图片的网址 : http://source.unsplash.com/random
code
http://127.0.0.1:8000/save?url=http://source.unsplash.com/random
http://127.0.0.1:8000/async?url=http://source.unsplash.com/random
service.py
from datetime import datetime
import time
import tornado.web
import tornado.httpclient
import tornado.gen
from .main import AuthBaseHandler
from utils.photo import ImageSave
from models.account import Posts
class URLSaveHandler(AuthBaseHandler):
"""保存指定url的图片 同步方法"""
@tornado.web.authenticated
def get(self, *args, **kwargs):
url = self.get_argument('url', None)
response = self.fetch_image(url) # 获取指定url的图片
if not response.body: # 数据被封装在响应对象的body属性中
self.write('empty data')
return
image_saver = ImageSave(self.settings['static_path'], 'x.jpg')
image_saver.save_image(response.body) # body 就是图片数据 保存图片
image_saver.make_thumbs() # 做缩略图
# 添加到数据库,拿到 post 实例
post = Posts.add_post_for(self.current_user, image_saver.image_url, image_saver.thumb_url)
print("-- {} -end fetch:#{}".format(datetime.now(), post.id))
self.redirect('/post/{}'.format(post.id)) # 跳转到 post 页面
def fetch_image(self, url):
"""获取指定url的图片"""
client = tornado.httpclient.HTTPClient() # 获取同步操作对象
print("-- {} -going to fetch:{}".format(datetime.now(), url))
response = client.fetch(url) # 获取url对应的内容 得到响应对象
return response
class AsyncURLSaveHandler(AuthBaseHandler):
"""保存指定url的图片 异步方法"""
@tornado.web.authenticated
@tornado.gen.coroutine
def get(self, *args, **kwargs):
url = self.get_argument('url', None)
response = yield self.fetch_image(url) # 获取指定url的图片
if not response.body: # 数据被封装在响应对象的body属性中
self.write('empty data')
return
image_saver = ImageSave(self.settings['static_path'], 'x.jpg')
image_saver.save_image(response.body) # 保存图片
image_saver.make_thumbs() # 缩略图
post = Posts.add_post_for(self.current_user, image_saver.image_url, image_saver.thumb_url) # 添加到数据库
print("-- {} -end fetch:#{}".format(datetime.now(), post.id))
self.redirect('/post/{}'.format(post.id))
@tornado.gen.coroutine
def fetch_image(self, url):
"""获取指定url的图片"""
client = tornado.httpclient.AsyncHTTPClient() # 获取异步操作对象
print("-- {} -going to fetch:{}".format(datetime.now(), url))
yield tornado.gen.sleep(6)
response = yield client.fetch(url) # 获取url对应的内容 得到响应对象
return response
app.py
import tornado.web
import tornado.options
import tornado.ioloop
from tornado.options import define, options
from handlers import main,auth,chat,service
define(name='port', default='8000', type=int, help='run port')
class Application(tornado.web.Application):
def __init__(self):
handlers = [
(r'/', main.IndexHandler),
(r'/explore', main.ExploreHandler),
(r'/post/(?P<post_id>[0-9]+)', main.PostHandler),
(r'/upload', main.UploadHandler),
(r'/profile', main.ProfileHandler),
(r'/login', auth.LoginHandler),
(r'/logout', auth.LogoutHandler),
(r'/signup', auth.SignupHandler),
(r'/room', chat.RoomHandler),
(r'/ws', chat.ChatSocketHandler),
(r'/save', service.URLSaveHandler),
(r'/async', service.AsyncURLSaveHandler),
]
settings = dict(
debug=True,
template_path='templates',
static_path='static',
login_url='/login',
cookie_secret='bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=',
pycket={
'engine': 'redis',
'storage': {
'host': 'localhost',
'port': 6379,
# 'password': '',
'db_sessions': 5, # redis db index
'db_notifications': 11,
'max_connections': 2 ** 30,
},
'cookies': {
'expires_days': 30,
},
}
)
super(Application, self).__init__(handlers, **settings)
application = Application()
if __name__ == '__main__':
tornado.options.parse_command_line()
application.listen(options.port)
print("Server start on port {}".format(str(options.port)))
tornado.ioloop.IOLoop.current().start()