同步与异步,回调与协程

时间:2022-03-15 23:35:07

同步与异步,回调与协程


  本文参考了下面的文章,案例也源自下面的文章:

  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)