python中的协程
协程的概念
谈到协程,就离不开异步和同步的概念,那我们就需要先明白异步和同步是什么意思,异步操作对于我们程序效率的提升有什么意义。
同步执行就是一个任务一个任务执行,只有在前一个任务执行结束之后才会开始执行下一个任务。这样做的坏处就是,一旦这些任务中有耗时的IO操作,cpu在等待IO的过程中会挂起,而在同步执行的情况下,cpu在挂起之后不会利用这段等待IO的时间去执行其他任务,而是硬等到IO结束才继续执行下一个任务,这样就会导致我们不能极致地利用cpu的性能,导致程序的效率不高。比如说我们写一段程序去读取两张excel表,如果每读一张的耗时是五秒,那同步执行的情况下,总耗时就是十秒。
异步执行区别于同步执行的地方就在于,当cpu在执行一些耗时操作的时候,它不会在原地傻傻等待,而是自己去找活干,在开始等待某一个耗时操作的之后,他会主动去执行其他任务。回到上一个例子,读取两张excel表,读取每一张耗时五秒,异步执行的情况下,cpu在读取第一张表后不会傻傻等待,而是会去执行下一个任务,读取另一张表,而最终的总耗时就是五秒多一点点。可见异步操作对我们程序效率的提升。
言归正传,那python中的协程对于我们程序的异步操作有什么用处呢?显然,协程的用处就是我们把同步执行升级为异步执行的手段之一。在python中有进程,线程,协程的概念。一个进程中可以有多个线程,一个线程中可以有多个协程。协程是一种轻量级的用户级线程,能够在单个线程内实现异步操作,由程序员主导它什么时候该主动让出执行权。协程的作用就是在线程中进一步利用cpu性能,在线程中出现多个耗时操作的时候,可以将这些耗时操作异步执行。
线程与协程的区别
讲到这里,有一些了解线程概念的读者就会有一个疑问,那多个线程不也是可以在进程中将一些耗时操作异步执行吗,为何要多此一举将线程再进一步细分为协程呢?
- 创建开销:协程是完全在用户态实现的,它不像线程需要操作系统内核的参与,所以创建协程的开销相对于创建线程来说会小很多。
- 上下文切换:而对于协程和线程的上下文切换,线程的上下文切换是比较耗时的操作,而且如果在线程抢占激烈的情况下,性能会有更明显的下降。而切换协程时,不需要保存和恢复内核态的上下文,相较于线程来说上下文切换开销很小。
- 调度机制:协程采用协作式调度,即协程在需要等待某个条件满足(如I/O操作完成、资源可用等)时,会主动让出CPU资源,这是协程的核心。而线程通常由操作系统进行抢占式调度,即线程的切换是由操作系统内核根据一定的调度策略自动进行的,这种方式可能导致线程在不需要等待任何条件时就被强制切换出去,徒增上下文切换的开销。
所以在有大量I/O密集型任务的情况下,合理使用协程可以让cpu发挥出更大的威力!
代码示例
同步执行
import time
def func(t):
print('耗时[{}秒]的操作开始执行'.format(t))
time.sleep(t)
print('耗时[{}秒]的操作结束执行'.format(t))
start = time.time()
func(3)
func(2)
func(2)
end = time.time()
print('总耗时:' + str(end - start))
输出:
可见同步执行的耗时是每一个耗时任务的总和,效率极低
使用协程异步执行
这里使用asyncio进行演示
import asyncio
import time
async def func(t):
print('耗时[{}秒]的操作开始执行'.format(t))
await asyncio.sleep(t) # 遇到耗时操作会自动让出执行权,由程序员自己控制!
print('耗时[{}秒]的操作结束执行'.format(t))
tasks = [
asyncio.ensure_future(func(3)),
asyncio.ensure_future(func(2)),
asyncio.ensure_future(func(2))
]
start = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
end = time.time()
print('总耗时:' + str(end - start))
输出:
可以看到在开始执行3秒的耗时操作之后,不会跟同步执行一样在原地傻傻等待,而是主动出让执行权,马上执行下一个2秒的耗时操作,这是由我们决定的,我们的await告诉了程序这是一个耗时操作,不必傻傻等待。而在执行第二个任务的时候也不会等待,发现需要等待就马上执行下一个任务。而在2秒的两个耗时操作都执行完了之后也不会结束,而是阻塞等待3秒的耗时任务执行完成之后再结束。
作者:李云龙炮击平安线程