由线程遗留下的问题:GIL导致多个线程不能真正的并行,CPython中多个线程不能并行
单线程实现并发:切换+保存状态
第一种方法:使用yield,yield可以保存状态。yield的状态保存与操作系统保存线程状态很像,但是yield是代码级控制的,更更轻量级。
# 两个计算任务一个采用生成器切换并发执行 一个直接串行调用
import time
def task1():
a= 0
for i in range(10000000):
a+=iyielddef task2():
g=task1()
b= 0
for i in range(10000000):
b+= 1next(g)
s=time.time()
task2()
print("并发执行时间",time.time()-s)
# 单线程下串行执行两个计算任务 效率反而比并发高 因为并发需要切换和保存
def task1():
a= 0
for i in range(10000000):
a+=i
def task2():
b= 0
for i in range(10000000):
b+= 1s=time.time()
task1()
task2()
print("串行执行时间",time.time()-s)
View Code
可以看出,对于纯计算任务而言,单线程并发反而使效率降低。而且使用yield来切换使得代码结构混乱。
第二种方法:使用greenlet模块,仅仅是对yield在封装。依然不能对检测IO操作。
import greenlet
def task1(name):
print("%s task1 run1" %name)
g2.switch(name) # 切换至任务2
print("task1 run2")
g2.switch() # 切换至任务2
def task2(name):
print("%s task2 run1" %name)
g1.switch() # 切换至任务1
print("task2 run2")
g1=greenlet.greenlet(task1)
g2=greenlet.greenlet(task2)
g1.switch("jerry") # 为任务传参数
View Code
第三种办法:协程
协程: 指的是一个线程完成并发执行,协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的
强调:
python的线程始于内核级别的,即由操作系统控制调度(遇到IO或执行事件过长就会被迫交出CPU执行权限,切换到其他线程)
单线程内开启协程,一旦遇到IO,就会从应用程序级别控制切换,以此来提升效率
优点:
1. 协程的切换开销更小,属于程序级别的切换,更加轻量级
2. 单线程内就可以实现并发的效果,最大限度的利用CPU
缺点:
1. 协程本质是单线程并发,无法利用多核,可以是一个程序开多个进程,每个进程内开启多个线程,每个线程内开启协程
2. 一旦协程遇到阻塞,将会阻塞整个线程
在CPython中,如果是计算密集型,使用协程是无法提高效率,反而因为切换任务导致效率较低,只能靠进程。IO密集型 多线程会比多进程效率高,因为多线程开销比多进程小的多。
使用模块:gevent,gevent既可以单线程实现并发,又可以检测IO操作
import gevent
import timefromgevent import monkey # 导入monkey补丁
monkey.patch_all() # 打补丁
def task1():
print("task1 run")
# gevent.sleep(3)
time.sleep(3)
print("task1 over")
def task2():
print("task2 run")
# gevent.sleep(1)
time.sleep(1)
print("task2 over")
g1=gevent.spawn(task1)
g2=gevent.spawn(task2)
# g1.join()
# g2.join()
gevent.joinall([g1,g2])
View Code
需要注意:
协程执行时必须对协程对象调用join函数(join让主线程等待协程任务完成,因为主线程一旦结束,协程任务就不执行了。反过来说 要保证协程任务正常执行结束 必须保证主线程没有结束)
如果有多个任务,随便调用哪一个的join都会并发执行所有任务,但是需要注意如果有一个存在IO操作的任务没有被join,该任务将无法正常执行完毕。
monkey补丁的原理,把原始的阻塞模块替换为修改后的非阻塞模块,来实现IO自定切换,所以打补丁的位置尽量要放在导入阻塞模块之前