1.协程
1.1引子
什么是并发?
cpu在执行多个线程任务,当一个线程任务遇到了io,操作系统自动将cpu从这个线程任务上切走,切到另一个线程任务,当这个任务在遇到io,再次切到其他线程任务…cpu在不同的线程任务上来回切换执行。
协程:一个线程实现并发的效果也就是一个线程绑定多个任务,在不同的人物之间来回切换。
并发的本质:切换+保持状态。
import time
def task1():
for i in range(1,11):
print(f'吃掉第{i}个苹果')
yield
def task2():
g = task1()
for i in range(1,11):
print(f'生产第{i}个苹果')
time.sleep(1)
next(g)
# task1()
task2()
上面的代码就是low版的协程,现在有10个IO密集型的任务,让你并发的去处理:
多进程的并发:操作系统控制:切换+保持状态。
多线程的并发:操作系统控制:切换+保持状态。
单线程并发(协程): 程序本身控制的:切换+保持状态。 最优的方案
协程,协程就是告诉Cpython解释器,你不是nb吗,不是搞了个GIL锁吗,那好,我就自己搞成一个线程让你去执行,省去你切换线程的时间,我自己切换比你切换要快很多,避免了很多的开销,对于单线程下,我们不可避免程序中出现io操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到io阻塞时就切换到另外一个任务去计算,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态,相当于我们在用户程序级别将自己的io操作最大限度地隐藏起来,从而可以迷惑操作系统,让其看到:该线程好像是一直在计算,io比较少,从而更多的将cpu的执行权限分配给我们的线程。
1.2协程的相关概念
-
协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。
协程的本质就是在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。为了实现它,我们需要找寻一种可以同时满足以下条件的解决方案:
- 可以控制多个任务之间的切换,切换之前将任务的状态保存下来,以便重新运行时,可以基于暂停的位置继续执行。
- 作为1的补充:可以检测io操作,在遇到io操作的情况下才发生切换
-
优点如下:
- 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
- 单线程内就可以实现并发的效果,最大限度地利用cpu
- 数据共享但是不会破坏数据,保证数据安全。
-
缺点如下:
- 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
- 协程如果遇到IO(即应用程序下面的所有任务同一时刻都遇到阻塞),则阻塞整个程序。
-
总结协程特点:
- 必须在只有一个单线程里实现并发
- 修改共享数据不需加锁
- 用户程序里自己保存多个控制流的上下文栈
- 协程如果遇到IO,则阻塞整个程序
1.3 实现协程的模块
1.3.1 greenlet
此模块是实现协程偏底层的模块,可以实现切换+保持状态,但是不能自动检测io。
from greenlet import greenlet
import time
def task1(name):
print(f'{name}正在LOL ....')
g2.switch('佘少杰')
print(f'{name}又在王者农药.....')
g2.switch()
def task2(name):
print(f'{name}准备要吃饭...')
# g1.switch()
time.sleep(1)
print(f'{name}准备上厕所...')
g1 = greenlet(task1)
g2 = greenlet(task2)
g1.switch('玮哥') # 启动切换 对象第一次调用此方法可以传参
1.3.2 Gevent
此模块是对greenlet模块封装升级,可以实现切换+保持状态,也可以自动检测io。
# import gevent
# from gevent import monkey, queue
# import time
# monkey.patch_all() # 让程序检测io,遇到io则切换
#
# q = queue.Queue(10)
#
# def task1(name1,name2):
# print(f'{name1,name2}正在LOL ....')
# time.sleep(2)
# print(f'又在王者农药.....')
#
#
# def task2():
# print(f'准备要吃饭...')
# time.sleep(1)
# print(f'准备上厕所...')
#
# g1 = gevent.spawn(task1,'barry','玮哥')
# g2 = gevent.spawn(task2)
# gevent.joinall([g1, g2])
# # 1. 启动分配的任务运行。
# # 2. 让主线程先执行分配的任务,将所以的分配的执行的任务执行完毕之后,在向下执行。
# # time.sleep(1)
# print('====主')
import gevent
from gevent import monkey, queue
import time
monkey.patch_all() # 让程序检测io,遇到io则切换
q = queue.Queue(10)
def task1():
for i in range(10):
print(f'向队列插入数值{i}')
q.put(i)
# time.sleep(0.5)
def task2():
for i in range(10):
print(f'从队列获取数值{i}')
q.get()
g1 = gevent.spawn(task1)
g2 = gevent.spawn(task2)
gevent.joinall([g1, g2])
# 1. 启动分配的任务运行。
# 2. 让主线程先执行分配的任务,将所以的分配的执行的任务执行完毕之后,在向下执行。
# time.sleep(1)
print('====主')
gevent:
- 优点:
- 可以实现切换+保持状态,并且可以预测io自动切换。
- 缺点:
- 如果使用monkey这个模块,他实际上是c语言的一个补丁,这个会修改socket等源码,方式不友好。
- 只能检测指定的IO,可能还存在一些问题。