9 应用异步和协程

为什么要用异步

一般代码的同步执行

同步异步通常用来形容一次方法调用。

  • 同步方法

    调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。

    每次只能向目标服务器发送一个请求,待其返回数据后才能进行下一次请求,若请求较多的情况下易发生阻塞。

  • 异步方法

    调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而异步方法通常会在另外一个线程中执行着。整个过程,不会阻碍调用者的工作。

    对于调用者来说,异步调用似乎是在一瞬间完成的。如果异步调用需要返回结果,那么当异步调用真实完成时,则会通知调用者。

    可同时发送多个请求到目标服务器,较早返回数据的将会被优先处理

举个例子

打个比方,比如我们去购物,如果你去商场实体店买一台空调,当你到了商场看中了一款空调,你就想售货员下单。售货员去仓库帮你调配物品。这天你热的实在不行了。就催着商家赶紧给你配送,于是你就等在商场里,候着他们,直到商家把你和空调一起送回家,一次愉快的购物就结束了。这就是同步调用。

不过,如果我们赶时髦,就坐再家里打开电脑,在网上订购了一台空调。当你完成网上支付的时候,对你来说购物过程已经结束了。虽然空调还没有送到家,但是你的任务都已经完成了。商家接到你的订单后,就会加紧安排送货,当然这一切已经跟你无关了,你已经支付完成,想什么就能去干什么了,出去溜达几圈都不成问题。等送货上门的时候,接到商家电话,回家一趟签收即可。这就是异步调用。

扫描二维码关注公众号,回复: 4099690 查看本文章

异步解决的问题:

异步处理可以让应用在长时间的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_objsome_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()

猜你喜欢

转载自blog.csdn.net/qq_14993591/article/details/83111240
9