美文网首页
异步IO、协程和爬虫

异步IO、协程和爬虫

作者: 小温侯 | 来源:发表于2018-07-20 23:56 被阅读156次

    补充:多进程和多线程的选择

    还记得多进程vs多线程吗?还记得CPU密集型和IO密集型吗?还记得GIL吗?

    由于GIL的存在,如果你的代码是CPU密集型的,那么多线程基本就是线性执行的,同时也只会占据在一个CPU core里;换句话说,在这种情况下多线程很鸡肋;再换句话说,如果你的作业是CPU密集型的,是不是应该考虑换个语言写?

    如果你的作业是计算密集型的,多进程就是个很好的选择。 尤其是当你的计算机CPU不止一个核心的时候。(那这里能不能用多线程?答案是能,但是多线程发挥不出多核的能力。)

    Python的多线程非常适合IO密集型的作业,值得一提的是面对IO密集型作业,多进程也是可以用的,多进程会比多线程更容易实现但消耗更多的系统资源。两者的效率是差不多的。因此,"IO密集=多线程",这句话其实是片面的。

    并发和并行

    不得不说这两个词翻译的非常好,但是,这样的翻译同时可造成了一定的迷惑。

    并发的英文是concurrency,而并行的英文是parallelism,它们其实是完全不同的两个单词。

    我之前看过一个比喻觉得非常形象,如果是你的作业是“吃苹果”(注意只是吃苹果,不是吃一个也不是吃三个。)并发就是三个人吃一个苹果,一个时间内只能一个人在吃;并行是三个人吃三个苹果,一个时间内三个各自吃自己的苹果,一起吃。

    专业一点描述的话,并发,时间段里有若干进程或线程争抢CPU资源,但是任何时间点上,每个CPU只能有一个进程或线程在执行,调度由CPU负责;并行,一个时间点上,有多个线程或进程同时在运行。

    这说明了一个问题,单核CPU是不存在并行的,多核CPU并发并行都存在。

    异步IO

    操作系统中,I/O设备分很多种:

    • 人机交互设备:打印机、显示器、键盘鼠标等
    • 存储设备:最多的就是磁盘
    • 网络通信设备:通过网络接口进行数据交换

    不管是哪一种,对于CPU运行速度来说,"I/O=慢"这句话总是成立的。当今的操作系统都已经支持了多进程多线程,就是用来解决这个问题的。如果一个IO被阻塞,系统会切换到其他的进程以便充分利用CPU,这就是异步IO。如果不切换,一直等待这个IO完成,就是同步IO。

    协程

    其实多进程和多线程已经解决了异步IO的问题。但是它们都存在一个限制,就是线程或进程的数量在一个操作系统中是有限的,同时它们切换也要消耗一定的资源。要知道很多时候我们要并发的代码块其实很小,比如说只是get一个网页,因此,人们会把这个代码块进一步的从线程中脱离出来,这就是协程,它更小巧,也像线程切换消耗资源。

    Python的协程

    首先关于Python中关于这部分的文档地址是:asyncio — Asynchronous I/O, event loop, coroutines and tasks

    参考如下代码:

    def h():
        for i in range(5):
            r = yield i
            print (r)
    
    c = h()
    g = c.send(None) # g = next(c)
    for i in range(4):
        print (g)
        g = c.send(10-i)
    

    得益于生成器的存在,如果利用好send()yield(),不仅可以实现两个代码块之间的调度,也可以实现两个代码块之间的消息传递。这就是Python协程的实现基础。

    在Python 3.4中,引入了asyncio标准库;在Python3.5中,又引入了新语法async和await关键字。我现在使用的是Python 3.6,这里我列举一下使用一般情况下,实现协程需要用到的方法和概念:

    • 协程coroutine,使用关键字async定义,一般是加在一个函数的def关键词前。和生成器一样,它在被调用的时候不会立刻执行,而是返回一个协程对象。
    • 任务task,是对协程对象的进一步封装,通常会包含各种状态值。
    • 时间循环event_loop,相当于携程的调度器,所有的任务或协程先放入循环中,再统一调度执行。循环可以接收协程对象,也可以接收任务对象
    • await关键字:用于阻塞当前协程
    • future对象,协程对象的未来状态,一般用来储存协程执行完成之后的状态。

    FYI,asyncio库做的是并发,不是并行。当然asyncio库远远不止这些内容,具体请自行阅读文档。

    下面来个综合一点的例子,另外推荐一篇很好的文章Python黑魔法 --- 异步IO( asyncio) 协程

    import asyncio
    import threading
    import time
    
    # 协程的函数
    async def c(idx):
        print ("[Coroutine] index: {}, threadID: {}".format(idx, threading.get_ident()))
        await asyncio.sleep(idx)
        return 'success'
    
    # 回调函数,future对象保存协程执行后的信息
    def callback(future):
        print ("Coro's callback function, result: {}, threadID: {}".format(future.result(), threading.get_ident()))
    
    print ("[Main thread] threadID: {}".format(threading.get_ident()))
    
    # 新建事件循环
    loop = asyncio.get_event_loop()
    
    # 新建协程
    coro = c(1)
    assert True == asyncio.iscoroutine(coro)
    
    # 新建任务,并加入回调函数
    task = loop.create_task(c(2))
    task.add_done_callback(callback)
    
    # 新建多个协程
    coros = [coro, task]
    for i in range (3,10,2):
        coros.append(c(i))
    
    start = time.time()
    
    # 开始调度
    loop.run_until_complete(asyncio.gather(*coros))
    
    print ("Finished in {}s".format(time.time() - start))
    # 关闭循环
    loop.close()
    
    # 输出
    # [Main thread] threadID: 21244
    # [Coroutine] index: 2, threadID: 21244
    # [Coroutine] index: 1, threadID: 21244
    # [Coroutine] index: 5, threadID: 21244
    # [Coroutine] index: 7, threadID: 21244
    # [Coroutine] index: 9, threadID: 21244
    # [Coroutine] index: 3, threadID: 21244
    # Coro's callback function, result: success, threadID: 21244
    # Finished in 9.003307104110718s
    

    可以看到:

    • 线程的ID都是一样的,说明这么多协程是在一条线程了完成的
    • 总执行时间是最长sleep那条协程的时间,说明这些协程是并发的。

    协程和爬虫

    回到爬虫的主题,之前所作的爬虫最大的限制就是并发http请求,这就是协程发挥作用的地方。当然你可以使用上文所述的方法来进行并发请求,你也可以用另一个库,名为aiohttp,它的描述是:Asynchronous HTTP Client/Server for asyncio and Python

    上了例子:

    import aiohttp
    import asyncio
    from bs4 import BeautifulSoup
    
    async def req(url, headers):
        # async with aiohttp.ClientSession(cookies, connector=proxy) as session:
        async with aiohttp.ClientSession() as session:
            async with session.get(url=url, headers=headers) as response:
                html = await response.text()
        soup = BeautifulSoup(html, 'lxml')
        print(soup.title.text)
    
    
    url = 'http://python.org'
    headers = {
        'content-type':
            'text/html; charset=utf-8'
        }
    
    # proxy = aiohttp.ProxyConnector(proxy="http://xxx.xxx.xxx.xxx:xxx")
    # cookies = {
    #     'cookie1':
    #        'cookie1_content'
    # }
    
    loop = asyncio.get_event_loop()
    
    coros = []
    for i in range(5):
        coros.append(req(url, headers))
    
    loop.run_until_complete(asyncio.gather(*coros))
    

    相关文章

      网友评论

          本文标题:异步IO、协程和爬虫

          本文链接:https://www.haomeiwen.com/subject/zpecmftx.html