学习笔记--协程
协程
协程的概念
协程,又称微线程。线程是系统级别的它们由操作系统调度,而协程则是程序级别的由程序根据需要自己调度。在一个线程中会有很多函数,我们把这些函数称为子程序,在子程序执行过程中可以中断去执行别的子程序,而别的子程序也可以中断回来继续执行之前的子程序,这个过程就称为协程。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。
yield实现协程
yield表达式有一个返回值,send方法的作用就是控制这个返回值,send的参数就是yield表达式的返回值。
生产者-消费者模型
def constomer():
r = ''
while True:
n = yield r
print(f'Constomer has take away {n}')
if not n:
break
def producer(c):
c.send(None) #启动生成器
n = 0
while n < 5:
n += 1
print(f'Producer starting {n}')
r = c.send(n)
c.close()
if __name__ == '__main__':
c = constomer()
producer(c)
yield from 两个的应用
1.拼接可迭代对象
astr = 'abc'
alist = [1,2,3]
adict = {"name":"anthony","age":"24"}
agen = (i for i in range(1,10,2))
def gen(*args):
for item in args:
yield from item
new_list = gen(astr,alist,adict,agen)
print(list(new_list))
2.生成器的嵌套
调用方:调用委派生成器的客户端(调用方)代码
委托生成器:包含yield from表达式的生成器函数;在调用方与子生成器之间建立一个双向通道.
子生成器:yield from后面加的生成器函数
#子生成器
def generator_1():
total = 0
average = 0
count = 0
while True:
num = yield average
if num is None:
break
count += 1
total += num
average = total/count
return total,count,average
#委托生成器
def proxy_gen():
while True:
#当子生成器结束了,yield from 左边的变量才会被赋值
total,count,average = yield from generator_1()
print(f'计算完毕!\n 总共传入{count},总和{total},平均值为{average}')
#调用方
def main():
calc_average = proxy_gen()
#启动生成器
next(calc_average)
print(calc_average.send(10.0))
print(calc_average.send(20.0))
print(calc_average.send(30.0))
#结束协程
calc_average.send(None)
if __name__ == '__main__':
main()
greenlet 实现协程
greenlet可以实现一个自行调度的微线程
from greenlet import greenlet
import time
def task1():
while True:
print('正在执行task1')
time.sleep(0.5) # 模拟堵塞
g2.switch() # 遇堵塞,切换到第二个任务
def task2():
while True:
print('正在执行task2')
time.sleep(0.5) # 模拟堵塞
g1.switch() #遇堵塞,切换到第一个任务
if __name__ == '__main__':
#创建greenlet对象
g1 = greenlet(task1)
g2 = greenlet(task2)
g1.switch() #开始任务
gevent 实现协程
能够自动识别程序中的耗时操作,在耗时的时候自动切换到其他任务
from gevent import monkey
import gevent
import time
monkey.patch_all()
def task1():
while True:
print('正在执行task1')
time.sleep(0.5)
# gevent.sleep(0.5)
def task2():
while True:
print('正在执行task2')
time.sleep(0.5)
if __name__ == '__main__':
# 遇到延迟才切换 如果没有遇到延迟的话 就不切换
#创建gevent对象 gevent.spawn(函数名,参数)
# g1 = gevent.spawn(task1)
# g2 = gevent.spawn(task2)
# #等主线程等待协程结束后退出
# g1.join()
# g2.join()
gevent.joinall(
[
gevent.spawn(task1),
gevent.spawn(task2)
]
)
IO模型
什么是IO?
我们都知道unix世界里、一切皆文件、而文件是什么呢?文件就是一串二进制流而已、不管socket、还是FIFO、管道、终端、对我们来说、一切都是文件、一切都是流、在信息交换的过程中、我们都是对这些流进行数据的收发操作、简称为I/O操作。在 I/O 设备上的输入和输出被处理为对相应的文件的读写操作。当打开一个文件时,内核会返回一个非负整数,用来标识该文件,称为文件描述符,简称fd。在此后的读、写等处理过程中,应用程序即可通过这个描述符来访问文件,而不需要记录有关文件的其他信息。
在网络编程中常用的套接字( socket )也是一种文件类型,一个套接字便是一个有着对应描述符的打开的文件,它用于和另外一个进程进行网络通信。
IO交互
内核空间中存放的是内核代码和数据、而进程的用户空间中存放的是用户程序的代码和数据、不管是内核空间还是用户空间、它们都处于虚拟空间中、Linux使用两级保护机制:0级供内核使用、3级供用户程序使用。
操作系统和驱动程序运行在内核空间、应用程序运行在用户空间、两者不能简单地使用指针传递数据、因为Linux使用的虚拟内存机制、其必须通过系统调用请求kernel来协助完成IO动作、内核会为每个IO设备维护一个缓冲区、用户空间的数据可能被换出、当内核空 间使用用户空间指针时、对应的数据可能不在内存中。
对于一个输入操作来说、进程IO系统调用后、内核会先看缓冲区中有没有相应的缓存数据、没有的话再到设备中读取、因为设备IO一般速度较慢、需要等待、内核缓冲区有数据则直接复制到进程空间。
所以、对于一个网络输入操作通常包括两个不同阶段:
(1)等待网络数据到达网卡 -----读取到内核缓冲区
(2)从内核缓冲区复制数据 ----- 用户空间
IO模型
常用模型分为5种:
1.阻塞IO
在内核将数据准备好之前,系统调用会一直等待所有的套接字,默认的是阻塞方式。
2.非阻塞IO
每次客户询问内核是否有数据准备好,即文件描述符缓冲区是否就绪。当有数据报准备好时,就进行拷贝数据报的操作。当没有数据报准备好时,也不阻塞程序,内核直接返回未准备就绪的信号,等待用户程序的下一个轮寻。
但是,轮寻对于CPU来说是较大的浪费,一般只有在特定的场景下才使用。
3.信号驱动IO
信号驱动IO模型,应用进程告诉内核:当数据报准备好的时候,给我发送一个信号,对SIGIO信号进行捕捉,并且调用我的信号处理函数来获取数据报。
4.IO多路复用
IO多路转接是多了一个select函数,select函数有一个参数是文件描述符集合,对这些文件描述符进行循环监听,当某个文件描述符就绪时,就对这个文件描述符进行处理。
其中,select只负责等,recvfrom只负责拷贝。
IO多路转接是属于阻塞IO,但可以对多个文件描述符进行阻塞监听,所以效率较阻塞IO的高。
5.异步IO
当应用程序调用aio_read时,内核一方面去取数据报内容返回,另一方面将程序控制权还给应用进程,应用进程继续处理其他事情,是一种非阻塞的状态。
当内核中有数据报就绪时,由内核将数据报拷贝到应用程序中,返回aio_read中定义好的函数处理程序。