协程
由于GIL锁导致CPython中多线程无法并发执行,只能并发执行。而并发实现的原理就是切换+保存,那就意味着多线程实现并发,就需要为每一个任务创建一个线程。那就出现了两个问题:
问题一:必然增加了线程创建销毁与切换带来的资源开销。
问题二:高并发情况下,由于任务数量太多导致无法开启新的线程,使得即没有实际任务要执行,也无法创建新线程来处理新任务的情况。
所以应想办法避免创建线程带来的问题,同时又能保证并发效果,协程就是使用单线程来实现多任务并发。 协程的本质是程序员自己通过代码自己检测程序中的IO,一旦遇到IO自己通过代码切换,给操作系统的感觉是你这个线程没有任何的IO。
ps:欺骗操作系统,让它误认为你这个程序一直没有IO,从而保证了程序在运行状态和就绪态来回切换,从而提升代码的运行效率。
单线程实现并发
并发 = 切换任务+保存状态。python中的生成器就具备这样一个特定,每次调用next都会回到生成器函数中执行代码,并且是基于上一次运行的结果,这就意味着生成器会自动切换任务并保存执行状态。
# 利用生成器实现单线程的并发效果
def task1():
while True:
yield
print("task1 run")
def task2():
g = task1()
while True:
next(g)
print("task2 run")
task2()
虽然我们通过生成器实现了并发效果,但是效率怎么样呢?
# 两个计算任务:一个采用生成器切换并发执行,一个串行调用
import time
def task1():
a = 0
for i in range(50000000):
a += i
yield
def task2():
g = task1()
b = 0
for i in range(50000000):
b += i
next(g)
start_time = time.time()
task2()
print("spend time",time.time()-start_time)
————————————————————————————————————————————
spend time 34.51863384246826
————————————————————————————————————————————
import time
def task1():
a = 0
for i in range(50000000):
a += i
def task2():
b = 0
for i in range(50000000):
b += i
start_time = time.time()
task1()
task2()
print('spend time:',time.time()-start_time)
————————————————————————————————————————————
spend time: 20.408362865447998
# 单线程下串行执行两个计算任务,效率反而比并发高,因为并发需要不停的来回保存和切换
可以看出对于纯计算密集型任务而言,单线程并发反而使执行效率下降了,所以单独使用协程并不适合处理计算型任务。而对于纯IO密集型任务而言,单线程并发将效率给提高了。
greenlet模块实现并发
当处理多个任务时,使用yield编程实现并发十分混乱,因此有人用greenlet模块专门对yield进行了封装。
import greenlet
def task1():
for i in range(20):
print("task1")
t2.switch() # 切换到任务t2
def task2():
for i in range(20):
print("task2")
t1.switch() # 切换到t1
t1 = greenlet.greenlet(task1)
t2 = greenlet.greenlet(task2)
t1.switch()
task1
task2
task1
task2
task1
task2
task1
task2
task1
task2
task1
task2
task1
task2
task1
task2
task1
task2
task1
task2
task1
task2
task1
task2
task1
task2
task1
task2
task1
task2
task1
task2
task1
task2
task1
task2
task1
task2
task1
task2
该模块简化了yield复杂的代码结构,实现了单线程下多任务并发,但是无论是直接使用yield还是greenlet模块都不能在遇到IO时自动切换任务,所以需要找到一个能够识别IO的一个工具,就有了geven模块。
gevent模块实现并发
常用方法
# 创建协程对象
g = gevent.spawn(func,*args,**kwargs)
# func是任务的函数名,args与kwargs为传入func中的不定长参数
g.join() # 等待g结束再执行主线程
gevent.join(g1,g2) # 等待g1,g2结束后再执行主线程
g.value() # 拿到func的返回值
遇到IO阻塞时会自动切换任务
# 第一个版本
from gevent import monkey;monkey.patch_all()
import time
import gevent
def task1():
print("task1 run")
time.sleep(3)
print("task1 end...")
def task2():
print("task2 run....")
time.sleep(2)
print("task2 end....")
g1 = gevent.spawn(task1)
g2 = gevent.spawn(task2)
# 执行以上代码并不会有任何打印信息,因为协程任务都是以异步方式来提交的,所以主线程会继续往下执行,执行完最后一行也就结束了。导致了协程让任务没有来的及执行,所以必须使用join来让主线程等待任务执行完毕
g1.join()
g2.join()
# 第二个版本
from gevent import monkey;monkey.patch_all() # 由于该模块经常被使用 所以建议写成一行
from gevent import spawn
import time
"""
注意gevent模块没办法自动识别time.sleep等io情况
需要你手动再配置一个参数
"""
def heng():
print("哼")
time.sleep(2)
print('哼')
def ha():
print('哈')
time.sleep(3)
print('哈')
def heiheihei():
print('嘿嘿嘿')
time.sleep(5)
print('嘿嘿嘿')
start = time.time()
g1 = spawn(heng)
g2 = spawn(ha) # spawn会检测所有的任务
g3 = spawn(heiheihei)
g1.join()
g2.join()
g3.join()
# heng()
# ha()
print(time.time() - start)
注意事项:
1.如果主进程结束了协程任务也会立即结束
2.monkey补丁的原理是把原始的阻塞方法替换为修改后的阻塞方法
3.必须在打补丁后再使用相应的功能
协程与线程比较
协程:是在单线程下实现并发,又称作为微线程,纤程。是一种用户态的轻量级线程,及协程是由用户程序自己控制调度的
线程:是属于内核级别的,由操作系统来控制调度(如单线程遇到IO或执行时间过长就会被迫交出cpu执行权限,切换到其他线程运行)
优点:
- 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
- 单线程内就可以实现并发的效果,最大限度第利用CPU
缺点:
- 协程本质就是在单线程下实现并发,无法利用多核CPU。可以是一个程序开启多个进程,每个进程开启多个线程,每个线程内开启协程来尽可能提高效率
- 协程一旦出现阻塞,将会阻塞整个线程。