Python协程(一) 概述
一、协程介绍
协程 ,又被称为微线程或者纤程,是一种用户态的轻量级线程,英文名Coroutine,它是实现多任务的一种方式。
其本质就是一个单线程,协程的作用就是在一个线程中人为控制代码块的执行顺序。
在一个线程中有很多函数,我们称这些函数为子程序。当一个子程序A在执行过程中可以中断执行,切换到子程序B,执行子程序B。而在适当的时候子程序B还可以切换回子程序A,去接着子程序A之前中断的地方(即回到子程序A切换到子程序B之前的状态)继续往下执行,这个过程,我们可以称之为协程。
二、Yield生成器的方式实现协程
在Python中,yield(生成器)可以很容易的实现上述的功能,从一个函数切换到另外一个函数。
由于比较繁琐,这里不再赘述,可以参考:https://blog.csdn.net/weixin_41599977/article/details/93656042
三、Greenlet模块
Greenlet是一个用C实现的协程模块,相比于python自带的yield,它可以使你在任意函数之间随意切换,而不需把这个函数先声明为generator。
安装:
pip3 install greenlet
from greenlet import greenlet import time def task_1(): while True: print("--This is task 1!--") g2.switch() # 切换到g2中运行 time.sleep(0.5) def task_2(): while True: print("--This is task 2!--") g1.switch() # 切换到g1中运行 time.sleep(0.5) if __name__ == "__main__": g1 = greenlet(task_1) # 定义greenlet对象 g2 = greenlet(task_2) g1.switch() # 切换到g1中运行
运行输出:
--This is task 1!-- --This is task 2!-- --This is task 1!-- --This is task 2!-- --This is task 1!-- --This is task 2!-- --This is task 1!-- --This is task 2!--
四、Gevent模块
Greenlet已经实现了协程,但是这个需要人工切换,很麻烦。
Python中还有一个能够自动切换任务的模块gevent,其原理是当一个greenlet遇到IO操作(比如网络、文件操作等)时,就自动切换到其他的greenlet,等到IO操作完成,在适当的时候切换回来继续执行。
由于IO操作比较耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程 ,就保证总有greenlet在运行,而不是等待IO。
安装:
pip3 install gevent
用法:
g1=gevent.spawn(func,1,2,3,x=4,y=5) # 创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的,spawn是异步提交任务 g2=gevent.spawn(func2) g1.join() #等待g1结束 g2.join() #等待g2结束 有人测试的时候会发现,不写第二个join也能执行g2,是的,协程帮你切换执行了,但是你会发现,如果g2里面的任务执行的时间长,但是不写join的话,就不会执行完等到g2剩下的任务了 #或者上述两步合作一步: gevent.joinall([g1,g2]) g1.value #拿到func1的返回值
遇到IO阻塞时会自动切换任务:
import gevent def eat(name): print('%s eat 1' % name) gevent.sleep(2) print('%s eat 2' % name) def play(name): print('%s play 1' % name) gevent.sleep(1) print('%s play 2' % name) g1 = gevent.spawn(eat, 'egon') g2 = gevent.spawn(play, name='egon') g1.join() g2.join() # 或者gevent.joinall([g1,g2])
上例gevent.sleep(2)
模拟的是gevent可以识别的IO阻塞。
而time.sleep(2)
或其他的阻塞,gevent是不能直接识别的。需要用下面一行代码打补丁,就可以识别了:
from gevent import monkey; monkey.patch_all() #必须放到被打补丁者的前面,如time,socket模块之前
或者我们干脆认为:要用gevent,需要将from gevent import monkey;monkey.patch_all()
放到文件的开头:
from gevent import monkey monkey.patch_all() # 必须写在最上面,这句话后面的所有阻塞全部能够识别了 import gevent # 直接导入即可 import time def eat(): # print() print('eat food 1') time.sleep(2) # 加上monkey就能够识别到time模块的sleep了 print('eat food 2') def play(): 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('主')
协程是通过自己的程序(代码)来进行切换的,只有遇到协程模块能够识别的IO操作的时候,程序才会进行任务切换,实现并发效果,如果所有程序都没有IO操作,那么就基本属于串行执行了。
五、Python3.x协程
Python3.x系列的协程有很多不同的地方,这里介绍下主要的:
1、asyncio
- asyncio是Python3.4引进的标准库,直接内置了对IO的支持,asyncio的操作,需要在coroutine中通过yield from完成。
import asyncio @asyncio.coroutine def get_body(i): print(f'start{i}') yield from asyncio.sleep(1) print(f'end{i}') loop = asyncio.get_event_loop() tasks = [get_body(i) for i in range(5)] loop.run_until_complete(asyncio.wait(tasks)) loop.close()
输出结果:
start4
start0
start1
start3
start2
end4
end0
end1
end3
end2
它的效果是和Gevent一样的,遇到IO操作的时候,自动切换上下文。
不同的是,它对tasks的操作:task先把这个5个参数不同的函数全部加载进来,然后执行任务,任务执行是无序的。
@asyncio.coroutine把一个generator标记为coroutine类型,然后把这个coroutine扔到eventloop中执行
yield from 语法让我们方便的调用另一个generator。由于asyncio.sleep()也是一个coroutine,线程不会等待,直接中断执行下一个消息循环。当asyncio.sleep()返回时,线程可以从yield from拿到返回值(此处是None),然后接着执行下一行语句。
2、async/await
在Python3.5的时候,asyncio
添加了两个关键字aysnc
和await
,让coroutine语法更简洁。async
关键字可以将一个函数修饰为协程对象,await
关键字可以将需要耗时的操作挂起,一般多是IO操作。import asyncio async def get_body(i): print(f'start{i}') await asyncio.sleep(1) print(f'end{i}') loop = asyncio.get_event_loop() tasks = [get_body(i) for i in range(5)] loop.run_until_complete(asyncio.wait(tasks)) loop.close()
运行结果:
start3
start4
start1
start0
start2
end3
end4
end1
end0
end2
Python3.7以后的版本使用asyncio.run即可。此函数总是会创建一个新的事件循环并在结束时关闭之。它应当被用作 asyncio 程序的主入口点,理想情况下应当只被调用一次。
import asyncio async def work(x): # 通过async关键字定义一个协程 for _ in range(3): print('Work {} is running ..'.format(x)) coroutine_1 = work(1) # 协程是一个对象,不能直接运行 # 方式一: loop = asyncio.get_event_loop() # 创建一个事件循环 result = loop.run_until_complete(coroutine_1) # 将协程对象加入到事件循环中,并执行 print(result) # 协程对象并没有返回结果,打印None # 方式二: # asyncio.run(coroutine_1) #创建一个新的事件循环,并以coroutine_1为程序的主入口,执行完毕后关闭事件循环
使用asyncio
实现的协程的一些特性:
- 使用
async
修饰返回的协程对象,不能直接执行,需要添加到事件循环event_loop
中执行。 - 协程主要是用于实现并发操作,其本质在同一时刻,只能执行一个任务,并非多个任务同时展开。
- 协程中被挂起的操作,一般都是异步操作,否则使用协程没有啥意义,不能提高执行效率。
- 协程是在一个单线程中实现的,其并发并未涉及到多线程。
六、为什么要使用协程
我们广泛使用的Python解释器是CPython,而CPython解释器中存在GIL锁,它的作用就是防止多线程时线程抢占资源,所以在同一时间只允许一个线程在执行,即使在多核CPU情况下也是一样 ,所以CPU的单核和多核对于多线程的运行效率并没有多大帮助,还要在线程之间的不停切换。
基于以上情况,在一些多线程的场景时,我们就可以使用协程来代替多线程,并且可以做的更灵活。我们下面来看下协程的优势:
1、线程是系统调度的,协程是程序员人为调度的,更加灵活,简化编程模型
2、与多线程相比,协程无需上下文切换的开销,避免了无意义的调度,提高了性能
3、与多线程相比,协程不需要像线程一样,无需原子操作锁定和同步的开销
所以,在处理一些高并发场景时,有时协程比多线程更加适合,比如做爬虫时。
参考文章:
https://www.jianshu.com/p/334388949ac9
https://blog.csdn.net/weixin_41599977/article/details/93656042
https://blog.csdn.net/weixin_44251004/article/details/86594117
https://www.cnblogs.com/cheyunhua/p/11017057.html
https://www.cnblogs.com/russellyoung/p/python-zhi-xie-cheng.html
https://www.cnblogs.com/dbf-/p/11143349.html