补充:多进程和多线程的选择
还记得多进程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))
网友评论