9.4 协程
9.4.1 协程初识
-
回顾串行,并行,并发
串行:多个任务执行时,第一个任务开始执行,遇到IO阻塞就等待,IO阻塞结束之后,继续执行下个任务
并行:多核,多个形成或者进程同时进行,4个CPU同时执行四个任务
并发:多个任务看起来同时执行,实质是CPU在多个任务之间来回切换(遇到IO阻塞,计算密集型执行时间过长)
并发的本质:
- 遇到IO阻塞,计算密集型执行时间过长,切换
- 保持线程或进程原来的状态
-
协程与多进程多线程比较
多进程:操作系统控制多个进程的多个任务切换+保持状态
多线程:操作系统控制多个线程的多个任务切换+保持状态
协程:程序控制一个线程的多个任务的切换以及保持状态
协程属于微并发,处理任务不易过多.
协程会调度CPU,如果协程管控的任务中,遇到阻塞,他会快速的(比操作系统快)切换到另一个任务,并且能将上一个任务挂机(保持状态),让操作系统以为CPU一直在工作.
-
协程基础知识
协程是单线程下的并发,又称为线程,纤程
什么是协程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的.
需要强调的是:
1. python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行) 2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关)
对比操作系统控制线程的切换,用户在单线程内控制协程的切换
优点如下:
1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
2. 单线程内就可以实现并发的效果,最大限度地利用cpu
缺点如下:
1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
总结协程特点:
- 必须在只有一个单线程里实现并发
- 修改共享数据不需加锁(优点)
- 用户程序里自己保存多个控制流的上下文栈
- 附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))
9.4.2 协程
- 计算密集型:串行与协程的效率对比
import time
def task1():
res = 1
for i in range(1,100000):
res += i
def task2():
res = 1
for i in range(1,100000):
res -= i
start_time = time.time()
task1()
task2()
print(f'串行消耗时间:{time.time()-start_time}')0.01754593849182129
import time
def task1():
res = 1
for i in range(1, 100000):
res += i
yield res
def task2():
g = task1()
res = 1
for i in range(1, 100000):
res -= i
next(g)
start_time = time.time()
task2()
print(f'协程消耗时间:{time.time() - start_time}')0.023561477661132812
- 协程:遇到IO自动切换
from greenlet import greenlet
import time
# 不能自动切换,
# 遇到IO不切换
# 可以保持原来的状态.
def eat(name):
print('%s eat 1' %name) #2
g2.switch('alex') #3
time.sleep(3)
print('%s eat 2' %name) #6
g2.switch() #7
def play(name):
print('%s play 3' %name) #4
g1.switch() #5
print('%s play 4' %name) #8
g1 = greenlet(eat)
g2 = greenlet(play)
g1.switch('太白') # 1 第一次切换一定要传参
g2.switch('b1')
time.sleep(300)
# 还没有做到真正遇到IO切换
import gevent
import time
def eat(name):
print('%s eat 1' %name) # 1
# gevent.sleep(2)
time.sleep(3)
print('%s eat 2' %name)
def play(name):
print('%s play 1' %name) # 2
# gevent.sleep(1)
time.sleep(2)
print('%s play 2' %name)
g1 = gevent.spawn(eat, 'alex')
g2 = gevent.spawn(play, name='taibai')
g1.join()
g2.join()
# 或者gevent.joinall([g1,g2])
gevent.joinall([g1,g2])
print('主')
import threading
from gevent import monkey
monkey.patch_all() # 将你代码中的所有的IO都标识.
import gevent # 直接导入即可
import time
def eat():
print(f'线程1:{threading.current_thread().getName()}')
print('eat food 1')
time.sleep(3) # 加上mokey就能够识别到time模块的sleep了
print('eat food 2')
def play():
print(f'线程2:{threading.current_thread().getName()}')
print('play 1')
time.sleep(1) # 来回切换,直到一个I/O的时间结束,这里都是我们个gevent做得,不再是控制不了的操作系统了。
print('play 2')
g1=gevent.spawn(eat)
g2=gevent.spawn(play)
gevent.joinall([g1,g2])
print(f'主:{threading.current_thread().getName()}')