同步与异步,回调与协程
本文参考了下面的文章,案例也源自下面的文章:
https://www.cnblogs.com/xybaby/p/6406191.html
为了深入理解JavaScript中的异步编程,我翻阅了一些资料,刚好看到这篇文章,里面有讲到异步编程。不过这篇文章中的案例是用Python编写的,我花了一些时间让Visual Studio Code能跑Python程序,顺便复习了Python。
本文将转述同步与异步,回调与协程的概念,也加入了一些自己的理解。
一、在Visual Studio Code中编写Python程序
之前,Visual Studio Code我都是用来编写JavaScript程序的,现在我要用它来编写Python程序,所以需要先做一些准备工作。
首先,在Visual Studio Code中,新建一个文件夹,然后打开这个文件夹,该文件夹将是Python程序的存放目录。
新建一个demo1.py文件。此时,Visual Studio Code检测到你想要进行Python编程,会推荐你安装若干个插件,我们都给它装上。
然后,在demo1.py中输入一个示例程序,如下:
#!/usr/bin/python # -*- coding: UTF-8 -*- import urllib2 import time # 以阻塞方式访问两个url def http_block_way(url1, url2): # 记录开始时间 begin = time.time() # 访问url1 data1 = urllib2.urlopen(url1).read() # 访问url2 data2 = urllib2.urlopen(url2).read() # 打印html文件大小 print len(data1), len(data2) # 打印耗费时间 print 'http_blockway cost', time.time() - begin url_list = ['https://www.baidu.com', 'https://www.bing.com'] # *会将数组转化为逗号分隔的参数列表 http_block_way(*url_list)
现在,准备运行这个文件。点击菜单“任务”->“配置默认生成任务”,会生成一个tasks.json文件,编辑内容如下:
{ "version": "2.0.0", "tasks": [ { "label": "python", "type": "shell", "command": "C:/Python27/python.exe", "args": [ "${file}" ], "group": { "kind": "build", "isDefault": true }, "presentation": { "reveal": "silent" } } ] }
完成配置后,在demo1.py文件中使用快捷键CTRL+SHIFT+B,即可运行程序。运行结果如下:
二、Python插件的安装
在示例程序中会用到若干个Python插件。
以前在Python中装插件,通常是下载插件包,例如tornado-5.0.2.tar.gz,解压后,分别执行命令:
python setup.py build
python setup.py install
就可以安装好插件了。
但如果插件又依赖于另外的包,就麻烦一点了。所以Python也有自己的插件管理程序,名为pip。
先下载pip,按传统方式安装完成后,就可以使用命令:
pip install tornado(需要先将C:\Python27\Scripts添加到PATH环境变量中)
来下载和安装插件了,非常简单。
还有一个问题,有一个插件在安装过程中提示:“Microsoft Visual C++ 9.0 is required”,这时需要安装微软的一个工具:https://www.microsoft.com/en-us/download/details.aspx?id=44266
三、同步与异步,回调与协程
1. 同步与异步
这里先说明两组概念:“同步与异步”、“阻塞与非阻塞”。
我们可以通过一个办理业务的流程来说明这两个概念。这两个概念面向的角色不同,“同步与异步”面向的是服务的请求者,“阻塞与非阻塞”面向的是服务的提供者。
例如我们去市政服务中心办理一个业务,这个业务流程涉及到多个步骤,先去A处办一个证,再到B处、C处分别办理其它业务,最后才能完成一整套流程。
去办事的人,称为“服务的请求者”。如果你必须等A处的事情办完才能接着去B处办事,这时的流程就是同步的,同步流程在时间线上是顺序执行的,先A后B后C,总耗费时间=A业务时间+B业务时间+C业务时间;如果可以在A处先把需要的材料交上,不用在原地等待他办完,可以先到B处接着办下一个业务,A处办理完业务后会用短信或其它方式通知你,这时的流程就是异步的,异步流程在时间线上可以并列执行,效率高于同步流程。
市政服务中心的工作人员,称为“服务的提供者”。如果他按顺序一个一个地服务于办事人,要等上一个办事人的事情全部办完才接着做下一个办事人的事情,这就是阻塞。如果改善一下流程,象新的麦当劳餐厅那样,你点完单就在一边等着,餐配好了会在电子告示牌上通知你,那就是非阻塞。(这让我想起以前在肯德基点餐的情形。以前每次点餐后,都是站在原地等着服务员配餐,后面的人只能干看着,每个人都要花3-5分钟,点个餐总得花个十几分钟,所以我都不爱去肯德基。不过听说肯德基现在的流程也改善了。)
2. 同步的代码实现
下面的代码演示了下载两个网页,采用同步方式:
#!/usr/bin/python # -*- coding: UTF-8 -*- import urllib2 import time # 以阻塞方式访问两个url def http_block_way(url1, url2): # 记录开始时间 begin = time.time() # 访问url1 data1 = urllib2.urlopen(url1).read() # 访问url2 data2 = urllib2.urlopen(url2).read() # 打印html文件大小 print len(data1), len(data2) # 打印耗费时间 print 'http_blockway cost', time.time() - begin url_list = ['https://www.baidu.com', 'https://www.bing.com'] # *会将数组转化为逗号分隔的参数列表 http_block_way(*url_list)
3. 异步的代码实现
在Python中,如果要使用异步发送HTTP请求,可以使用tornado插件。
tornado有一个AsyncHTTPClient对象,用于发送异步的HTTP请求,该对象的fetch()方法可以下载网页内容。fetch()方法的参数有两个:一个是url,另一个是回调函数,在请求完成时被调用。
#!/usr/bin/python # -*- coding: UTF-8 -*- import tornado from tornado.httpclient import AsyncHTTPClient import time import sys # 以异步回调方式访问两个url def http_callback_way(url1, url2): http_client = AsyncHTTPClient() begin = time.time() count = [0] def handle_result(response, url): print('%s : handle_result with url %s' % (time.time(), url)) count[0] += 1 if count[0] == 2: print 'http_callback_way cost', time.time() - begin sys.exit(0) # fetch函数有两个参数,第一个是url,第二个是回调函数 # 访问url1 http_client.fetch(url1, lambda res, u=url1: handle_result(res, u)) print('%s here between to request' % time.time()) # 访问url2 http_client.fetch(url2, lambda res, u=url2: handle_result(res, u)) url_list = ['https://www.baidu.com', 'https://www.bing.com'] http_callback_way(*url_list) tornado.ioloop.IOLoop.instance().start()
上述代码中,有一处大家可能会感觉陌生,就是:
http_client.fetch(url1, lambda res, u=url1: handle_result(res, u))
lambda是函数表达式,语法格式为:
lambda [函数的参数]: [函数的代码]
如下面的lambda:
func = lambda x: x + 1
它相当于:
def func(x): return (x + 1)
4. 协程的代码实现
回调对于编程而言,将发起请求的代码和请求完成的处理代码割裂了,我们不能一眼看清请求的流程。当回调较多时,代码 会变得难以理解。
协程的出现解决了这个问题。使用协程,你的代码看上去是同步的,而实际的执行却是异步的。
协程可以使用Python原生的generator(生成器),也可以使用更专业的gevent插件。
使用generator:
#!/usr/bin/python # -*- coding: UTF-8 -*- import tornado import sys import time from tornado.httpclient import AsyncHTTPClient from tornado import gen # 以协程方式访问两个url,使用原生的generator def http_generator_way(url1, url2): begin = time.time() count = [0] @gen.coroutine def do_fetch(url): http_client = AsyncHTTPClient() response = yield http_client.fetch(url, raise_error=False) print url, response.error count[0] += 1 if count[0] == 2: print 'http_generator_way cost', time.time() - begin sys.exit(0) do_fetch(url1) do_fetch(url2) url_list = ['https://www.baidu.com', 'https://www.bing.com'] http_generator_way(*url_list) tornado.ioloop.IOLoop.instance().start()
使用gevent:
#!/usr/bin/python # -*- coding: UTF-8 -*- import gevent import time from gevent import monkey import urllib2 # 以协程方式访问两个url,使用gevent def http_coroutine_way(url1, url2): monkey.patch_all() begin = time.time() def looks_like_block(url): data = urllib2.urlopen(url).read() print url, len(data) gevent.joinall([gevent.spawn(looks_like_block, url1), gevent.spawn(looks_like_block, url2)]) print('http_coroutine_way cost', time.time() - begin) url_list = ['https://www.baidu.com', 'https://www.bing.com'] http_coroutine_way(*url_list)