异步IO、协程和爬虫

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

还记得多进程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、协程和爬虫)