并行和并发
表示程序/计算机具有处理多个任务的能力
并行表示可以同时处理多个任务(几个多核CPU)
并发无法同时处理多个任务,但是可以基于时间片轮转法在多任务间快速切换的执行任务。
同步和异步
在基于并行或者并发处理任务的时候,任务中如果出现阻塞操作,就可以选择使用同步或者异步的方式进行处理阻塞操作。
同步处理:让CPU等待阻塞操作结束后,在继续执行处理阻塞后面的其他操作。
异步处理:遇到阻塞操作当前任务会交出CPU的使用权,让CPU可以继续处理其他任务。
因此,在异步应用的过程中,如果一个任务出现了阻塞操作,可以将阻塞操作单独封装成一个单独的任务。
进程和线程
是用来实现异步的两种技术手段
在异步应用的过程中,如果一个任务出现了阻塞操作,可以将阻塞操作单独封装成一个单独的任务,这个任务就是进程或者线程。
协程(重要!)
我们知道,无论是多进程还是多线程,在遇到IO阻塞时都会被操作系统强行剥夺走CPU的执行权限(使得cup执行其他操作,其他操作可能是我们程序的其他部分,也可能是其他的应用程序),我们自己程序的执行效率因此就降低了下来。
解决这一问题的关键在于:
我们自己从自己的应用程序级别检测到IO阻塞,然后使得cpu切换到我们自己程序的其他部分/任务执行(这里的任务指的是当前我们自己程序表示的进程或线程中的某一组操作/子程序),这样可以把我们程序的IO阻塞降到最低,我们的程序处于就绪态就会增多,以此来迷惑操作系统,操作系统便以为我们的程序是IO阻塞比较少的程序,从而会尽可能多的分配CPU给我们,这样也就达到了提升程序执行效率的目的。
通俗理解:
一个线程/进程可以表示一组指定行为的操作,这个操作可以由多个执行步骤组成,这些执行步骤有的是阻塞操作有的非阻塞操作,那么,当cpu执行当前进程/线程的时候遇到了阻塞的执行步骤的时候,如果不对其处理,则包含当前执行步骤的进程/线程就会被挂起进入到阻塞状态,且交出cpu的使用权(cpu就被别人抢走了)。那么如果遇到阻塞的执行步骤,我们的程序可以检测出它是阻塞的,且可以将cpu切换到我们自己程序其他非阻塞的执行步骤时,则包含这些执行步骤的进程/线程就不会进入到阻塞状态,从而减少进程/线程的阻塞状态,增加就绪状态(牢牢抢占cup)极大幅度提升程序执行的效率。
因此,有了协程后,在单进程或者单线程的模式下,就可以大幅度提升程序的运行效率了!
在python3.5之后新增了asyncio模块,可以帮我们检测IO(只能是网络IO【HTTP连接就是网络IO操作】),实现应用程序级别的切换(异步IO)。
接下来让我们来了解下协程的实现,从 Python 3.4 开始,Python 中加入了协程的概念,但这个版本的协程还是以生成器对象为基础的,在 Python 3.5 则增加了 asyncio,使得协程的实现更加方便。首先我们需要了解下面几个概念:
特殊函数:
在函数定义前添加一个async关键字,则该函数就变为了一个特殊的函数!
特殊函数的特殊之处是什么?
1.特殊函数被调用后,函数内部的程序语句(函数体)没有被立即执行
2.特殊函数被调用后,会返回一个协程对象
协程:
协程对象,特殊函数调用后就可以返回/创建了一个协程对象。
协程对象 == 特殊的函数 == 一组指定形式的操作
协程对象 == 一组指定形式的操作
任务:
任务对象就是一个高级的协程对象。高级之处,后面讲,不着急!
任务对象 == 协程对象 == 一组指定形式的操作
任务对象 == 一组指定形式的操作
事件循环:
事件循环对象(Event Loop),可以将其当做是一个容器,该容器是用来装载任务对象的。所以说,让创建好了一个或多个任务对象后,下一步就需要将任务对象全部装载在事件循环对象中。
思考:为什么需要将任务对象装载在事件循环对象?
当将任务对象装载在事件循环中后,启动事件循环对象,则其内部装载的任务对象对应的相关操作就会被立即执行。
import asyncio
import time
特殊的函数
async def get_request(url):
print('正在请求的网址是:',url)
time.sleep(2)
print('请求网址结束!')
return 123
创建了一个协程对象
c = get_request('www.1.com')
创建任务对象
task = asyncio.ensure_future(c)
创建事件循环对象
loop = asyncio.get_event_loop()
将任务对象装载在loop对象中且启动事件循环对象
loop.run_until_complete(task)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
任务对象对比协程对象的高级之处重点在于:
可以给任务对象绑定一个回调函数!
回调函数有什么作用?
回调函数就是回头调用的函数,因此要这么理解,当任务对象被执行结束后,会立即调用给任务对象绑定的这个回调函数!
import asyncio
import time
特殊的函数
async def get_request(url):
print('正在请求的网址是:',url)
time.sleep(2)
print('请求网址结束!')
return 123
回调函数的封装:必须有一个参数
def t_callback(t):
#参数t就是任务对象
# print('回调函数的参数t是:',t)
# print('我是任务对象的回调函数!')
data = t.result()#result()函数就可以返回特殊函数内部的返回值
print('我是任务对象的回调函数!,获取到特殊函数的返回值为:',data)
创建协程对象
c = get_request('www.1.com')
创建任务对象
task = asyncio.ensure_future(c)
给任务对象绑定回调函数:add_done_callback的参数就是回调函数的名字
task.add_done_callback(t_callback)
创建事件循环对象
loop = asyncio.get_event_loop()
loop.run_until_complete(task)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
多任务的协程
import asyncio
import time
start = time.time()
urls = [
'www.1.com','www.2.com','www.3.com'
]
async def get_request(url):
print('正在请求:',url)
time.sleep(2)
print('请求结束:',url)
有了三个任务对象和一个事件循环对象
if name == 'main':
tasks = []
for url in urls:
c = get_request(url)
task = asyncio.ensure_future(c)
tasks.append(task)
#将三个任务对象,添加到一个事件循环对象中
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
print('总耗时:',time.time()-start)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
出现两个问题:
1.没有实现异步效果
2.wait()是什么意思?
wait()函数:
给任务列表中的每一个任务对象赋予一个可被挂起的权限!当cpu执行的任务对象遇到阻塞操作的时候,当前任务对象就会被挂起,则cup就可以执行其他任务对象,提高整体程序运行的效率!
挂起任务对象:让当前正在被执行的任务对象交出cpu的使用权,cup就可以被其他任务组抢占和使用,从而可以执行其他任务组。
注意:特殊函数内部,不可以出现不支持异步模块的代码,否则会中断整个异步效果!
await关键字:挂起发生阻塞操作的任务对象。在任务对象表示的操作中,凡是阻塞操作的前面都必须加上await关键字进行修饰!
完整的实现了,多任务的异步协程操作
import asyncio
import time
start = time.time()
urls = [
'www.1.com','www.2.com','www.3.com'
]
async def get_request(url):
print('正在请求:',url)
# time.sleep(2) #time模块不支持异步
await asyncio.sleep(2)
print('请求结束:',url)
有了三个任务对象和一个事件循环对象
if name == 'main':
tasks = []
for url in urls:
c = get_request(url)
task = asyncio.ensure_future(c)
tasks.append(task)
#将三个任务对象,添加到一个事件循环对象中
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
print('总耗时:',time.time()-start)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
真正的将多任务的异步协程作用在爬虫中
需求:爬取自己服务器中的页面数据,并将其进行数据解析操作
aiohttp:是一个基于网络请求的模块,功能和requests相似,但是,requests是不支持异步的,而aiohttp是支持异步的模块。
环境安装:pip install aiohttp
具体用法:
1.先写大致架构
with aiohttp.ClientSession() as sess:
#基于请求对象发起请求
#此处的get是发起get请求,常用参数:url,headers,params,proxy
#post方法发起post请求,常用参数:url,headers,data,proxy
#发现处理代理的参数和requests不一样(注意),此处处理代理使用proxy='http://ip:port'
with sess.get(url=url) as response:
page_text = response.text()
#text():获取字符串形式的响应数据
#read():获取二进制形式的响应数据
return page_text
1
2
3
4
5
6
7
8
9
10
2.在第一步的基础上补充细节
在每一个with前加上async关键字
在阻塞操作前加上await关键字
完整代码:
async def get_request(url):
#requests是不支持异步的模块
# response = await requests.get(url=url)
# page_text = response.text
#创建请求对象(sess)
async with aiohttp.ClientSession() as sess:
#基于请求对象发起请求
#此处的get是发起get请求,常用参数:url,headers,params,proxy
#post方法发起post请求,常用参数:url,headers,data,proxy
#发现处理代理的参数和requests不一样(注意),此处处理代理使用proxy='http://ip:port'
async with await sess.get(url=url) as response:
page_text = await response.text()
#text():获取字符串形式的响应数据
#read():获取二进制形式的响应数据
return page_text
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
多任务异步爬虫的完整代码实现:
import requests
import asyncio
import time
from lxml import etree
import aiohttp
start = time.time()
urls = [
'http://127.0.0.1:5000/bobo',
'http://127.0.0.1:5000/jay',
'http://127.0.0.1:5000/tom'
]
该任务是用来对指定url发起请求,获取响应数据
async def get_request(url):
#requests是不支持异步的模块
# response = await requests.get(url=url)
# page_text = response.text
#创建请求对象(sess)
async with aiohttp.ClientSession() as sess:
#基于请求对象发起请求
#此处的get是发起get请求,常用参数:url,headers,params,proxy
#post方法发起post请求,常用参数:url,headers,data,proxy
#发现处理代理的参数和requests不一样(注意),此处处理代理使用proxy='http://ip:port'
async with await sess.get(url=url) as response:
page_text = await response.text()
#text():获取字符串形式的响应数据
#read():获取二进制形式的响应数据
return page_text
def parse(t):#回调函数专门用于数据解析
#获取任务对象请求到的页面源码数据
page_text = t.result()
tree = etree.HTML(page_text)
a = tree.xpath('//a[@id="feng"]/@href')[0]
print(a)
tasks = []
for url in urls:
c = get_request(url)
task = asyncio.ensure_future(c)
task.add_done_callback(parse)
tasks.append(task)
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
print('总耗时:',time.time()-start)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
异步爬虫具体应用
注意:通常,异步操作主要作用在耗时环节。