11-05 协程
一. 什么是协程?
# 知识储备:
进程: 资源单位. 多进程下实现并发. 如果多核就是出现并行
线程: 执行单位. 同一进程下的多线程实现并发.
# 协程: (提示: 这个概念完全是程序员自己意淫出来的根本不存在)
协程就是在单线程下实现并发
二. 为什么要用协程?
# 知识储备: 多道技术.
多道计数的核心就是切换+保存状态
切换分2种情况:
1) 程序在运行的过程中遇到了IO
2) 程序的执行时间过长或者有一个优先级更高的程序替代了它
# 为什么要用协程?
多道技术可以控制内核级别程序遇到IO或执行时间过长的情况下保存状态以后剥夺程序的CPU执行权限, 进而提升程序的执行效率.
我们可以在单线程内开启协程, 控制应用程序代码级别遇到IO情况下保存状态以后切换, 以此来提升效率. (提示: 如果是非IO操作的切换与效率无关)
# 协程的优点: 应用程序级别速度要远远高于操作系统的切换
# 协程的缺点: 多个任务一旦有一个阻塞没有切,整个线程都阻塞在原地, 该线程内的其他的任务都不能执行了.
# 强调!!!:
一旦引入协程,就需要检测单线程下所有的IO行为,
实现遇到IO就切换,少一个都不行,以为一旦一个任务阻塞了,整个线程就阻塞了,
其他的任务即便是可以计算,但是也无法运行了
验证: 切换是否就一定提升效率
我们可以基于yield来验证。yield本身就是一种在单线程下可以保存任务运行状态的方法.
# 知识回顾
'''
# 1. yiled可以保存状态,yield的状态保存与操作系统的保存线程状态很像,但是yield是代码级别控制的,更轻量级
# 2. send可以把一个函数的结果传给另外一个函数,以此实现单线程内程序之间的切换
'''
# 串行执行计算密集型的任务
import time
def func1():
for i in range(10000000):
i + 1
def func2():
for i in range(10000000):
i + 1
start_time = time.time()
func1()
func2()
print(time.time() - start_time) # 执行时间: 1.1209993362426758
# 切换 + yield
import time
def func1():
while True:
10000000 + 1
yield
def func2():
g = func1() # 先初始化出生成器
for i in range(10000000):
i + 1
next(g)
start_time = time.time()
func2()
print(time.time() - start_time) # 执行时间: 1.4919734001159668
# 总结由此而知: 如果是非IO操作的切换与效率无关
'''
yield缺陷: yield不能检测IO,实现遇到IO自动切换. 接下来我们使用第三方gevent模块实现
'''
三. 使用第三方gevent模块实现单线程下的协程
1. 安装
# 前提: 安装了环境变量. 这里使用的是清华的源地址, 默认国外的地址下载速度太慢了!!
pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple gevent
2. 用法
Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程, 在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
g1 = gevent.spawn(func, 1,, 2, 3, x = 4, y = 5)创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的. spawn内部调用了g.start()是一种类始于开启进程的异步提交任务的操作.
g2 = gevent.spawn(func2)
g1.join() # 等待g1结束
g2.join() # 等待g2结束
# 或者上述两步合作一步:gevent.joinall([g1,g2])
g1.value # 拿到func1的返回值
3. 协程实现
'''
# spawn /spɔːn/ 再生侠 闪灵悍将 繁衍
# patch /pætʃ/ 补丁 修补 修补文件
注意!!!: from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块之前. 因为gevent在没有打补丁的情况下只能识别gevent自带的IO操作.
'''
from gevent import monkey;monkey.patch_all()
import time
import gevent
"""
gevent模块本身无法检测常见的一些io操作
在使用的时候需要你额外的导入一句话
from gevent import monkey
monkey.patch_all()
又由于上面的两句话在使用gevent模块的时候是肯定要导入的
所以还支持简写
from gevent import monkey;monkey.patch_all()
"""
def heng():
print('哼')
time.sleep(2)
print('哼1')
def ha():
print('哈')
time.sleep(3)
print('哈1')
def heiheihei():
print('heiheihei')
time.sleep(5)
print('heiheihei1')
# 情况1: 单线程默认执行情况下耗时统计
'''
start_time = time.time()
heng()
ha()
heiheihei()
print(time.time() - start_time) # 10.006284236907959
'''
# 情况2: 单线程使用gevent实现协程遇到IO切换+保存状态耗时统计
# 第一种写法:
'''
start_time = time.time()
g1 = gevent.spawn(heng) # 内部使用了g.start()
g2 = gevent.spawn(ha)
g3 = gevent.spawn(heiheihei)
g1.join()
g2.join() # 等待被检测的任务执行完毕 再往后继续执行
g3.join()
print(time.time() - start_time) # 5.006734848022461
'''
# 第二种写法: 如果是多个spawn就不要直接在后面使用join了, 不然会变成串行执行.而是使用第一种和第二种方式.
'''
start_time = time.time()
gevent.spawn(heng).join() # 内部使用了g.start()
gevent.spawn(ha).join()
gevent.spawn(heiheihei).join()
print(time.time() - start_time) # 10.006813526153564
'''
# 第三种写法: 是基于第一种写法的简写
start_time = time.time()
g1 = gevent.spawn(heng) # 内部使用了g.start()
g2 = gevent.spawn(ha)
g3 = gevent.spawn(heiheihei)
gevent.joinall([g1, g2, g3])
print(time.time() - start_time) # 5.006734848022461
四. 协程应用: 使用gevent模块实现单线程下的socket并发
通过gevent实现单线程下的socket并发(from gevent import monkey;monkey.patch_all()一定要放到导入socket模块之前,否则gevent无法识别socket的阻塞
1. TCP服务端
from gevent import monkey; monkey.patch_all()
import gevent
from socket import *
'''
如果不想用money.patch_all()打补丁,可以用gevent自带的socket
from gevent import socket
s=socket.socket()
'''
def communication(conn):
while True:
try:
data_bytes = conn.recv(1024)
if not data_bytes:
break
conn.send(data_bytes.upper())
except ConnectionResetError as e:
print(e)
break
conn.close()
def server_forever(ip, port): # forever /fərˈevə(r)/ 永远 直到永远 永恒
server = socket(AF_INET, SOCK_STREAM)
server.bind((ip, port))
server.listen(5)
while True:
conn, client_address = server.accept()
# 检测communication中的recv的或者send的IO行为.(主要检测accept)
gevent.spawn(communication, conn)
if __name__ == '__main__':
'''
网络号为127的地址保留用于环回测试本机的进程间通信(127.0.0.0到127.255.255.255是保留地址,用于环回测试,0.0.0.0到0.255.255.255也是保留地址,用于表示所有的IP地址。
'''
# 检测server_forever中的accept的IO行为.
g = gevent.spawn(server_forever, '127.0.0.2', 8080)
g.join() # 等待g运行结束. 也就是说一直运行server_forever中的True循环中的代码. 如果这里不指定上面的spawn是异步提交的任务, 整个程序会直接结束.
2. TCP客户端
# 多线程并发多个客户端
from threading import Thread
from threading import current_thread
from socket import *
def client_communication(ip, port):
client = socket(AF_INET, SOCK_STREAM)
client.connect((ip, port))
count = 0
while True:
client.send(f'{current_thread().name} say hello!'
f' {count}'.encode('utf-8'))
count += 1
data_bytes = client.recv(1024)
print(data_bytes)
client.close()
if __name__ == '__main__':
for i in range(100):
t = Thread(target=client_communication, args=('127.0.0.2', 8080))
t.start()
五. 总结
理想状态, 我们可以通过:
多进程下面开设多线程
多线程下面再开设协程序
从而使我们的程序执行效率提升