并发: 一个时间段内,有几个程序在同一个cpu上运行,但是任意时刻只有一个程序在cpu上运行
并行: 任意时刻点上, 有多个程序同时运行在多个cpu上
实际举例说明:
问题 - 喝茶
情况: 开水没有,水壶要洗,茶壶茶杯要洗;火生类, 茶叶也有了。怎么办?
所需时间:
洗水壶: 3
灌凉水: 1
洗茶壶: 3
洗茶杯: 3
拿茶叶: 1
泡茶: 1
烧开水: 30
并发版本:
老赵(cpu1):
办法1: 洗水壶,灌凉水, 放在火上;等待水烧开的时间里, 洗水壶,洗茶杯,拿茶叶;等水开了,泡茶喝。
总用时: 3+1+30+1 = 35
办法2: 先做好一些准备工作, 洗水壶,洗茶壶茶杯,拿茶叶;也切就绪,灌水烧水;坐等水开了泡茶喝。
总用时: 3+3+3+1+30+1 = 41
办法3: 洗净水壶,灌上凉水,放在火上,坐等水开;水开之后, 急急忙忙找茶叶,洗茶壶茶杯,泡茶喝。
总用时: 3+1+30+3+3+1 = 41
并行版本:
老赵(cpu1):洗好水壶,灌上凉水,放在火上
老李(cpu2):洗茶壶
老李(cpu2):洗茶壶
老谢(cpu3):洗茶杯
总用时: 31
同步: 代码调用IO操作时,必须等待IO操作完成才返回的调用方式
异步: 代码调用IO操作时,不必等IO操作完成就返回的调用方式
阻塞: 调用函数时,当前线程被挂起 (比如前面socket编程中socket.connect就是阻塞的)
非阻塞: 调用函数时,当前线程不会被挂起,而是立即返回
例子:
小乐爱喝茶,废话不说,煮开水。
出场人物:小乐,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
1 小乐把水壶放到火上,立等水开。(同步阻塞)
—— 小乐觉得自己有点傻
2 小乐把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)
—— 小乐还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出呜呜~~~~的噪音。
3 小乐把响水壶放到火上,立等水开。(异步阻塞)
—— 小乐觉得这样傻等意义不大
4 小乐把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)
—— 小乐觉得自己聪明了。
所谓同步异步,只是对于水壶而言。
普通水壶,同步;响水壶,异步。
虽然都能干活,但响水壶可以在自己完工之后,提示小乐水开了。这是普通水壶所不能及的。
同步只能让调用者去轮询自己(情况2中),造成小乐效率的低下。
所谓阻塞非阻塞,仅仅对于小乐而言。
—— 立等的小乐,阻塞;看电视的小乐,非阻塞。
情况1和情况3中小乐就是阻塞的,媳妇喊他都不知道。
虽然3中响水壶是异步的,可对于立等的小乐没有太大的意义。
所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。
1)阻塞I/O(blocking I/O)
2)非阻塞I/O (nonblocking I/O)
3)I/O复用(select 和poll) (I/O multiplexing)
4)信号驱动I/O (signal driven I/O (SIGIO))
5)异步I/O (asynchronous I/O (the POSIX aio_functions))
前四种都是同步,只有最后一种才是真正的异步IO。
阻塞I/O:
简介:进程会一直阻塞,直到数据拷贝完成
描述:应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好。
如果数据没有准备好,一直等待….数据准备好了,从内核拷贝到用户空间,IO函数返回成功指示。
非阻塞IO:
简介:非阻塞IO通过进程反复调用IO函数(多次系统调用,并马上返回);在数据拷贝的过程中,进程是阻塞的;
描述:我们把一个SOCKET接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,
不要将进程睡眠,而是返回一个错误。这样我们的I/O操作函数将不断的测试数据是否已经准备好,
如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU的时间。
IO复用:
简介:主要是select和epoll;对一个IO端口,两次调用,两次返回,
比阻塞IO并没有什么优越性;关键是能实现同时对多个IO端口进行监听;
描述:I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,
但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,
多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。
信号驱动IO:
简介:两次调用,两次返回;
描述:首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。
当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。
异步IO模型
简介:数据拷贝的时候进程无需阻塞。
描述:当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,
通过状态、通知和回调来通知调用者的输入输出操作同步IO引起进程阻塞,直至IO操作完成。
异步IO不会引起进程阻塞。IO复用是先通过select调用阻塞。
5种I/O模型的比较:
1.处理多个描述字时,比如同时处理套接字和磁盘IO、终端IO
2.一个客户同时处理多个套接字
3.服务器既要处理监听套接字,又要处理已连接套接字
4.既要处理TCP、也要处理UDP
5.一个服务器要处理多个服务和协议
I/O多路复用不局限于网络编程,也可以用于其他程序。
epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现
select:
select函数进行IO复用服务器模型的原理是:
当一个客户端连接上服务器时,服务器就将其连接的fd加入fd_set集合,
等到这个连接准备好读或写的时候,就通知程序进行IO操作,与客户端进行数据通信。
大部分 Unix/Linux 都支select函数,该函数用于探测多个文件句柄的状态变化。
这样所带来的缺点是:
1、 单个进程可监视的fd数量被限制,即能监听端口的大小有限。
一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.
2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:
当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
poll:
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,
然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,
如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,
被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。
它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:
1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
2、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
epoll:
epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,
并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,
一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知
epoll的优点:
1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;
即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
3、 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。
在高并发情况下, 连接活跃度不是很高, epoll比select好
在并发不高情况下, 连接活跃度高, select比epoll好
先用非阻塞IO的方式实现 模拟http请求:
import socket
from urllib.parse import urlparse
def get_url(url):
# 通过socket请求html
url = urlparse(url)
host = url.netloc
path = url.path
if path == "":
path = '/'
# 建立socket连接
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.setblocking(False) # 非阻塞IO
try:
client.connect((host, 80))
except BlockingIOError as e:
pass
while True:
try:
client.send(
"GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode('utf8'))
break
except OSError as e:
pass
data = b""
while True:
try:
d = client.recv(1024)
except BlockingIOError as e:
continue
if d:
data += d
else:
break
data = data.decode('utf8')
html_data = data.split('\r\n\r\n')[1] # 把请求头信息去掉, 只要网页内容
print(html_data)
client.close()
if __name__ == '__main__':
get_url('http://www.baidu.com')
结论:
在模拟http请求的情形下:
在client.setblocking(False)设置为非阻塞IO之后, client.connect和 client.send依然需要不停的尝试,
直到成功。实际效果并不比阻塞式IO好。
接下来尝试使用select/epoll方法:
# 通过非阻塞IO实现http请求
# select/epoll + 回调 + 事件循环方式编码
# 好处: 并发性高
# 在抓取多个网站时,使用单线程代替了多线程。省去了多线程间切换和计算资源的开销。
import socket
from urllib.parse import urlparse
from selectors import DefaultSelector, EVENT_WRITE, EVENT_READ
# DefaultSelector会自动选择select还是epoll,windows是select,linux是epoll。但最好还是linux下运行
selector = DefaultSelector()
urls = ['http://www.baidu.com']
stop = False
class Fetcher:
def connected(self, key):
selector.unregister(key.fd)
self.client.send(
"GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(self.path, self.host).encode('utf8'))
selector.register(self.client.fileno(), EVENT_READ, self.readable)
def readable(self, key):
d = self.client.recv(1024)
if d:
self.data += d
else:
selector.unregister(key.fd)
data = self.data.decode('utf8')
html_data = data.split('\r\n\r\n')[1] # 把请求头信息去掉, 只要网页内容
print(html_data)
self.client.close()
urls.remove(self.spider_url)
if not urls:
global stop
stop = True
def get_url(self, url):
self.spider_url = url
url = urlparse(url)
self.host = url.netloc
self.path = url.path
self.data = b""
if self.path == "":
self.path = '/'
# 建立socket连接
self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.client.setblocking(False)
try:
self.client.connect((self.host, 80))
except Exception as e:
pass
# 注册到select中去
selector.register(self.client.fileno(), EVENT_WRITE, self.connected)
def loop():
# 事件循环,不停请求socket状态并调用对应的回调函数
# 1. secletor本身不支持register模式
# 2. socket状态变化后的回调是由程序员完成的
while not stop:
ready = selector.select()
for key, mask in ready:
call_back = key.data
call_back(key)
# 回调+事件循环+select(poll/epoll)
if __name__ == '__main__':
fetcher = Fetcher()
fetcher.get_url('http://www.baidu.com')
loop() # 事件循环
根据select/epoll的回调编码方式, 我们可以看出:
1.常规的编码方式是从上而下的,理解比较容易。但是回调的编码方式,把代码割得四分五裂,难以理解。
2.回调的编码方式在代码出错时,查错会变得非常麻烦。
3.回调的共享变量非常麻烦,只有都处于同一个类中,或者变量全部使用全局变量。
总结:
1.可读性差
2.共享状态管理困难
3.异常处理困难
针对回调之痛,我们将引入协程来解决。
1. 线程是由操作系统切换的,单线程切换意味着我们需要程序员自己去调度任务。
2. 不在需要锁,并发性高。如果单线程内切换函数,性能会远高于多线程间切换,并发性高。
我们在编写爬虫时,需要先抓取网页源码和解析源码。但在抓取源码时,往往有等待网站服务器响应时间, 我们在等待的时间里想要先让函数处于暂停状态,去执行另一个爬取的函数,伪代码:
def get_url(url):
html1 = get_html(url) # 当爬取源码等待响应时,暂停当前函数,去爬取html2
urls = parse_url(html)
def get_url(url):
html2 = get_html(url)
urls = parse_url(html2)
# 1 生成器不只可以产生值,还可以接收值。对应的 启动生成器有两种方式 next和send
def gen_func():
cannon1 = yield 1 # 按顺序接受send的传入值
print(cannon1)
yield 2
yield 3
if __name__ == '__main__':
gen = gen_func()
print(next(gen))
# 在调用send发送非None值之前, 我们必须启动一次生成器, 方式有两种 1. gen.send(None) 2. next(gen)
print(gen.send('cannon'))
# send 方法可以将值传入生成器内部,同时还会重启暂停的函数,相当于执行一次next
print(next(gen))
print(gen)
运行结果:
1
cannon
2
3
def gen_func():
yield 'http://projectsedu.com'
yield 2
yield 3
return 'cannon'
if __name__ == '__main__':
gen = gen_func()
next(gen)
gen.close()
next(gen) # gen_func中还有yield 再调用next就会报异常
运行结果会报错, 因为close后 又执行了next
Traceback (most recent call last):
File "tempCodeRunnerFile.py", line 12, in <module>
next(gen) # gen_func中还有yield 再调用next就会报异常
StopIteration
def gen_func():
yield 'http://projectsedu.com'
yield 2
yield 3
return 'cannon'
if __name__ == '__main__':
gen = gen_func()
print(next(gen))
print(next(gen))
gen.throw(Exception, 'download error') # 抛自定义异常
运行结果, 紧随yield 2 后面报异常
http://projectsedu.com
2
Traceback (most recent call last):
File "/3.gen_throw.py", line 12, in <module>
gen.throw(Exception, 'download error')
File "3.gen_throw.py", line 3, in gen_func
yield 2
Exception: download error
yield from 是python3.3 新加的语法, 在讲解该语法前,我们先演示一段代码:
from itertools import chain
my_list = [1, 2, 3]
my_dict = {
'cannon1': 'http://projectsedu.com',
'cannon2': 'http://www.baidu.com',
}
for value in chain(my_list, my_dict, range(5, 10)):
print(value)
我们通过chain 可以让不同的迭代类型一起被遍历, 得到结果如下:
1
2
3
cannon1 # 字典类型被遍历 只遍历key 不是value
cannon2
5
6
7
8
9
那么chain是如何实现的呢? 下面我们自己来实现一个my_chain:
def my_chain(*args, **kwargs):
for my_iterable in args:
for value in my_iterable:
yield value
for value in my_chain(my_list, my_dict, range(5, 10)):
print(value)
结果一致,不展示了。 yield from 语法 可让my_chain函数简化代码:
def my_chain(*args, **kwargs):
for my_iterable in args:
yield from my_iterable # yield from 用法展示
# for value in my_iterable:
# yield value
for value in my_chain(my_list, my_dict, range(5, 10)):
print(value)
可以看到 yield from my_iterable
就是 for value in my_iterable: yield value
的简化。当然yield from
的应用远没那么简单, 下面我们依然通过代码对它进一步讲解
final_result = {}
# 称为子协程
def sales_sum(pro_name):
total = 0
nums = []
while True:
x = yield
print(pro_name+'销量:', x)
if not x: # send(None)跳出
break
total += x
nums.append(x)
return total, nums
# 称为调用方
def middle(key):
while True:
final_result[key] = yield from sales_sum(key)
print(key+'销量统计完成!')
def main():
data_sets = {
'面膜': [1200, 1500, 1300],
'手机': [28, 55, 98, 108],
'大衣': [280, 560, 778, 70]
}
for key, data_set in data_sets.items():
print('start key:', key)
m = middle(key)
m.send(None) # 预激活middle协程 yield from sales_sum(key)开始接受数据
for value in data_set:
m.send(value) # 给协程传递每一组值
m.send(None) # 在sales_sum 中 跳出while True
print('final_result:', final_result)
if __name__ == '__main__':
main()
这是一个统计销量的代码, 我们得到结果:
start key: 面膜
面膜销量: 1200
面膜销量: 1500
面膜销量: 1300
面膜销量: None
面膜销量统计完成!
start key: 手机
手机销量: 28
手机销量: 55
手机销量: 98
手机销量: 108
手机销量: None
手机销量统计完成!
start key: 大衣
大衣销量: 280
大衣销量: 560
大衣销量: 778
大衣销量: 70
大衣销量: None
大衣销量统计完成!
final_result: {'面膜': (4000, [1200, 1500, 1300]), '手机': (289, [28, 55, 98, 108]), '大衣': (1688, [280, 560, 778, 70])}
你可能会有疑惑, 为什么要有中间middle并放入yield from来实现这样的功能,之间在main函数中调用sales_sum函数不行吗?
其实yield from帮我们自动解决了很多的报错, 如果直接调用sales_sum没有middle,我们需要增加很多代码来处理报错。举例:
def sales_sum(pro_name):
total = 0
nums = []
while True:
x = yield
print(pro_name+'销量:', x)
if not x: # send(None)跳出
break
total += x
nums.append(x)
return total, nums
if __name__ == '__main__':
my_gen = sales_sum('手机')
my_gen.send(None) # 预激活生成器
my_gen.send(1200)
my_gen.send(1300)
my_gen.send(1222)
my_gen.send(999)
my_gen.send(None)
得到的结果(如下)会出现报StopIteration错,而yield from内部会帮我们自动try except处理:
手机销量: 1200
手机销量: 1300
手机销量: 1222
手机销量: 999
手机销量: None
Traceback (most recent call last):
File 4.yieldfrom_example.py", line 47, in
my_gen.send(None)
StopIteration: (4721, [1200, 1300, 1222, 999])
生成器实现协程会带来一个问题: 你的代码中yield是用于生成器还是用于协程?
为了让编写的代码可读性更好,python3.5之后引入了async和await实现协程。
async和await的用法,我们通过上一个yield from的代码例子的修改来说明:
import types
final_result = {}
# 不同于yield from , await调用的子生成器函数 需要加装饰器@types.coroutine
@types.coroutine
def sales_sum(pro_name):
total = 0
nums = []
while True:
x = yield
print(pro_name+'销量:', x)
if not x: # send(None)跳出
break
total += x
nums.append(x)
return total, nums
# async中不能 有yield
# await只能出现async的函数中, await可以看作是yield from的替代
async def middle(key):
while True:
final_result[key] = await sales_sum(key)
print(key+'销量统计完成!')
def main():
data_sets = {
'面膜': [1200, 1500, 1300],
'手机': [28, 55, 98, 108],
'大衣': [280, 560, 778, 70]
}
for key, data_set in data_sets.items():
print('start key:', key)
m = middle(key)
m.send(None) # 预激活middle协程 yield from sales_sum(key)开始接受数据
for value in data_set:
m.send(value) # 给协程传递每一组值
m.send(None) # 在sales_sum 中 跳出while True
print('final_result:', final_result)
if __name__ == '__main__':
main()
结果和之前使用yield from实现一样。
start key: 面膜
面膜销量: 1200
面膜销量: 1500
面膜销量: 1300
面膜销量: None
面膜销量统计完成!
start key: 手机
手机销量: 28
手机销量: 55
手机销量: 98
手机销量: 108
手机销量: None
手机销量统计完成!
start key: 大衣
大衣销量: 280
大衣销量: 560
大衣销量: 778
大衣销量: 70
大衣销量: None
大衣销量统计完成!
final_result: {'面膜': (4000, [1200, 1500, 1300]), '手机': (289, [28, 55, 98, 108]), '大衣': (1688, [280, 560, 778, 70])}